Compare commits

...

9 Commits

Author SHA1 Message Date
OlgunR
45011122b2 Refactor API clients to use primary constructor for HttpClient
Refactored CatalogApiClient, DashboardApiClient, LayoutApiClient, and MassDataApiClient to use C# primary constructor syntax for injecting HttpClient. Removed private _httpClient fields and updated all usages to reference the constructor parameter directly. This change simplifies the code and modernizes dependency injection without altering any API logic.
2026-05-11 17:08:52 +02:00
OlgunR
1b67d0472e Refactor EditFormTemplate context initialization
Refactored the EditFormTemplate block to separate SetEditContext and editModel initialization for improved clarity. Removed the call to SetPopupHeaderText(editModel.IsNew) from this section to streamline context setup and avoid potential logic issues.
2026-05-11 16:39:45 +02:00
OlgunR
a007842ab0 Refactor Amount field to use decimal and DxSpinEdit
Replaced string-based Amount handling with a decimal property in the edit model. Switched input from DxTextBox to DxSpinEdit, removing manual parsing and validation logic for Amount. This improves data binding, input reliability, and code clarity.
2026-05-11 16:28:55 +02:00
OlgunR
bf98432e20 Refactor query string construction in GetAllAsync
Refactored the GetAllAsync method to use a Dictionary and QueryHelpers.AddQueryString for building query strings, replacing manual string concatenation. This improves code clarity and reduces the risk of formatting errors.
2026-05-11 15:59:07 +02:00
OlgunR
d4b7f02c5e Move band layout init to OnAfterRenderAsync on first render
Moved InitializeBandLayoutAsync() from OnInitializedAsync() to OnAfterRenderAsync() in CatalogsGrid.razor and MassDataGrid.razor. Now initialization occurs only on first render, followed by StateHasChanged(), to ensure proper layout setup after initial rendering. This addresses potential timing or rendering issues.
2026-05-11 15:47:00 +02:00
OlgunR
a0297d40a8 Refactor API base URL config to use AppSettings object
Replaced direct configuration access for the API base URL with retrieval from an AppSettings object. Updated HttpClient configuration to use appSettings.ApiBaseUrl, improving consistency and maintainability. Existing AppSettings DI registration is retained.
2026-05-11 15:13:28 +02:00
OlgunR
1c00449186 Improve error handling for problem details deserialization
Explicitly catch JsonException and NotSupportedException when reading problem details from HTTP responses. Add comments to clarify that these errors are ignored since problem details are optional, making error handling more precise and avoiding unintended exception swallowing.
2026-05-11 14:24:46 +02:00
OlgunR
8a22217866 Refactor BandGridBase methods to async for UI reliability
Refactored several methods in BandGridBase<TItem> to async Task and updated their invocations to use await. EventCallbacks for date filter changes now use async lambdas. Awaited InvokeAsync(StateHasChanged) to ensure UI updates after async operations. These changes improve UI state consistency and reliability in Blazor.
2026-05-11 13:57:01 +02:00
OlgunR
a6a17991bb Refactor theme change handler for safe async updates
Refactored OnThemeChanged in MainLayout.razor to use InvokeAsync for proper synchronization of UI updates and async logic, preventing threading issues. Also added a blank line after app.Run() in Program.cs (no functional impact).
2026-05-11 13:36:44 +02:00
10 changed files with 71 additions and 88 deletions

View File

@@ -132,13 +132,13 @@ public abstract class BandGridBase<TItem> : ComponentBase
UpdateBandOptions();
}
protected void RemoveBand(BandDefinition band)
protected async Task RemoveBand(BandDefinition band)
{
bandLayout.Bands.Remove(band);
foreach (var key in columnBandAssignments.Where(p => p.Value == band.Id).Select(p => p.Key).ToList())
columnBandAssignments.Remove(key);
UpdateBandOptions();
SyncBandsFromAssignments();
await SyncBandsFromAssignments();
}
protected void UpdateBandCaption(BandDefinition band, string value)
@@ -147,19 +147,19 @@ public abstract class BandGridBase<TItem> : ComponentBase
UpdateBandOptions();
}
protected void UpdateColumnBand(string fieldName, string? bandId)
protected async Task UpdateColumnBand(string fieldName, string? bandId)
{
if (string.IsNullOrWhiteSpace(bandId))
columnBandAssignments.Remove(fieldName);
else
columnBandAssignments[fieldName] = bandId;
SyncBandsFromAssignments();
await SyncBandsFromAssignments();
}
protected string GetColumnBand(string fieldName)
=> columnBandAssignments.TryGetValue(fieldName, out var bandId) ? bandId : string.Empty;
protected void SyncBandsFromAssignments()
protected async Task SyncBandsFromAssignments()
{
foreach (var band in bandLayout.Bands)
{
@@ -168,7 +168,7 @@ public abstract class BandGridBase<TItem> : ComponentBase
.Select(c => c.FieldName)
.ToList();
}
_ = InvokeAsync(StateHasChanged);
await InvokeAsync(StateHasChanged);
}
protected void UpdateBandOptions()
@@ -245,8 +245,8 @@ public abstract class BandGridBase<TItem> : ComponentBase
if (!_dateFilterTemplates.TryGetValue(fieldName, out var template))
{
// EventCallbacks einmalig erstellen stabile Referenzen über alle Renders
_fromCallbacks[fieldName] = EventCallback.Factory.Create<DateTime?>(this, (DateTime? v) => OnFilterFromChanged(fieldName, v));
_toCallbacks[fieldName] = EventCallback.Factory.Create<DateTime?>(this, (DateTime? v) => OnFilterToChanged(fieldName, v));
_fromCallbacks[fieldName] = EventCallback.Factory.Create<DateTime?>(this, async (DateTime? v) => await OnFilterFromChanged(fieldName, v));
_toCallbacks[fieldName] = EventCallback.Factory.Create<DateTime?>(this, async (DateTime? v) => await OnFilterToChanged(fieldName, v));
template = BuildDateFilterTemplate(fieldName);
_dateFilterTemplates[fieldName] = template;
}
@@ -330,19 +330,19 @@ public abstract class BandGridBase<TItem> : ComponentBase
else if (op.OperatorType == BinaryOperatorType.Less) to = dt.AddDays(-1);
}
private void OnFilterFromChanged(string fieldName, DateTime? value)
private async Task OnFilterFromChanged(string fieldName, DateTime? value)
{
_filterFrom[fieldName] = value;
ApplyDateFilter(fieldName);
await ApplyDateFilter(fieldName);
}
private void OnFilterToChanged(string fieldName, DateTime? value)
private async Task OnFilterToChanged(string fieldName, DateTime? value)
{
_filterTo[fieldName] = value;
ApplyDateFilter(fieldName);
await ApplyDateFilter(fieldName);
}
private void ApplyDateFilter(string fieldName)
private async Task ApplyDateFilter(string fieldName)
{
var ops = new List<CriteriaOperator>();
if (_filterFrom.TryGetValue(fieldName, out var from) && from.HasValue)
@@ -360,7 +360,7 @@ public abstract class BandGridBase<TItem> : ComponentBase
gridRef?.SetFieldFilterCriteria(fieldName, criteria);
if (_filterContexts.TryGetValue(fieldName, out var ctx))
ctx.FilterCriteria = criteria;
_ = InvokeAsync(StateHasChanged);
await InvokeAsync(StateHasChanged);
}
protected void SetEditContext(EditContext context)

View File

@@ -117,7 +117,8 @@ else
</Columns>
<EditFormTemplate Context="editFormContext">
@{
SetEditContext(editFormContext.EditContext); var editModel = (CatalogEditModel)editFormContext.EditModel; SetPopupHeaderText(editModel.IsNew);
SetEditContext(editFormContext.EditContext);
var editModel = (CatalogEditModel)editFormContext.EditModel;
}
<DxFormLayout ColCount="2">
<DxFormLayoutItem Caption="Titel">
@@ -173,12 +174,16 @@ else
protected override async Task OnInitializedAsync()
{
await InitializeBandLayoutAsync();
await LoadCatalogs();
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await InitializeBandLayoutAsync();
StateHasChanged();
}
await ApplyGridLayoutAfterRenderAsync();
}

View File

@@ -50,13 +50,14 @@
await ApplyDxDarkOverrideAsync();
}
private async void OnThemeChanged()
private void OnThemeChanged()
{
InvokeAsync(async () =>
{
StateHasChanged();
if (_isInteractive)
{
await ApplyDxDarkOverrideAsync();
}
});
}
private async Task ApplyDxDarkOverrideAsync()

View File

@@ -137,7 +137,7 @@ else
<DxTextBox @bind-Text="editModel.CustomerName" Width="100%" />
</DxFormLayoutItem>
<DxFormLayoutItem Caption="Amount">
<DxTextBox @bind-Text="editModel.AmountText" Width="100%" />
<DxSpinEdit @bind-Value="editModel.Amount" MinValue="0" DisplayFormat="c2" Width="100%" />
</DxFormLayoutItem>
<DxFormLayoutItem Caption="Category">
<DxTextBox @bind-Text="editModel.Category" Width="100%" ReadOnly="@(!editModel.IsNew)" />
@@ -210,12 +210,16 @@ else
protected override async Task OnInitializedAsync()
{
await InitializeBandLayoutAsync();
await LoadPage(0);
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await InitializeBandLayoutAsync();
StateHasChanged();
}
await ApplyGridLayoutAfterRenderAsync();
}
@@ -281,7 +285,7 @@ else
{
Id = item.Id,
CustomerName = item.CustomerName,
AmountText = item.Amount.ToString("0.00"),
Amount = item.Amount,
Category = item.Category,
StatusFlag = item.StatusFlag,
UpdateProcedure = procedureOptions[0].Value,
@@ -298,12 +302,6 @@ else
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);
@@ -317,7 +315,7 @@ else
var dto = new MassDataWriteDto
{
CustomerName = editModel.CustomerName,
Amount = amount,
Amount = editModel.Amount,
Category = editModel.Category,
StatusFlag = editModel.StatusFlag
};
@@ -353,7 +351,7 @@ else
{
public int Id { get; set; }
public string CustomerName { get; set; } = string.Empty;
public string AmountText { get; set; } = string.Empty;
public decimal Amount { get; set; }
public string Category { get; set; } = string.Empty;
public bool StatusFlag { get; set; }
public int UpdateProcedure { get; set; }

View File

@@ -13,12 +13,12 @@ builder.Services.AddDevExpressBlazor(options => options.BootstrapVersion = Boots
builder.Services.AddScoped<ThemeState>();
builder.Services.AddScoped<BandLayoutService>();
var apiBaseUrl = builder.Configuration["ApiBaseUrl"];
builder.Services.Configure<AppSettings>(builder.Configuration);
var appSettings = builder.Configuration.Get<AppSettings>() ?? new AppSettings();
void ConfigureClient(HttpClient client)
{
if (!string.IsNullOrWhiteSpace(apiBaseUrl))
client.BaseAddress = new Uri(apiBaseUrl);
if (!string.IsNullOrWhiteSpace(appSettings.ApiBaseUrl))
client.BaseAddress = new Uri(appSettings.ApiBaseUrl);
}
builder.Services.AddHttpClient<ICatalogApiClient, CatalogApiClient>(ConfigureClient);

View File

@@ -1,4 +1,5 @@
using System.Net;
using System.Text.Json;
namespace DbFirst.BlazorWebApp.Services;
@@ -25,7 +26,14 @@ internal static class ApiClientHelper
problemDetail = problem.Detail ?? problem.Type;
}
}
catch { }
catch (JsonException)
{
// Ignoriere Fehler beim Lesen der Problem-Details, da sie optional sind
}
catch (NotSupportedException)
{
// Ignoriere Fehler beim Lesen der Problem-Details, da sie optional sind
}
var status = response.StatusCode;
var reason = response.ReasonPhrase;

View File

@@ -3,30 +3,24 @@ using DbFirst.Contracts.Catalogs;
namespace DbFirst.BlazorWebApp.Services;
public class CatalogApiClient : ICatalogApiClient
public class CatalogApiClient(HttpClient httpClient) : ICatalogApiClient
{
private readonly HttpClient _httpClient;
private const string Endpoint = "api/catalogs";
public CatalogApiClient(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task<List<CatalogReadDto>> GetAllAsync(CancellationToken ct = default)
{
var result = await _httpClient.GetFromJsonAsync<List<CatalogReadDto>>(Endpoint, ct);
var result = await httpClient.GetFromJsonAsync<List<CatalogReadDto>>(Endpoint, ct);
return result ?? [];
}
public async Task<CatalogReadDto?> GetByIdAsync(int id, CancellationToken ct = default)
{
return await _httpClient.GetFromJsonAsync<CatalogReadDto>($"{Endpoint}/{id}", ct);
return await httpClient.GetFromJsonAsync<CatalogReadDto>($"{Endpoint}/{id}", ct);
}
public async Task<ApiResult<CatalogReadDto?>> CreateAsync(CatalogWriteDto dto, CancellationToken ct = default)
{
var response = await _httpClient.PostAsJsonAsync(Endpoint, dto, ct);
var response = await httpClient.PostAsJsonAsync(Endpoint, dto, ct);
if (response.IsSuccessStatusCode)
{
var payload = await response.Content.ReadFromJsonAsync<CatalogReadDto>();
@@ -39,7 +33,7 @@ public class CatalogApiClient : ICatalogApiClient
public async Task<ApiResult<bool>> UpdateAsync(int id, CatalogWriteDto dto, CancellationToken ct = default)
{
var response = await _httpClient.PutAsJsonAsync($"{Endpoint}/{id}", dto, ct);
var response = await httpClient.PutAsJsonAsync($"{Endpoint}/{id}", dto, ct);
if (response.IsSuccessStatusCode)
{
return ApiResult<bool>.Ok(true);
@@ -51,7 +45,7 @@ public class CatalogApiClient : ICatalogApiClient
public async Task<ApiResult<bool>> DeleteAsync(int id, CancellationToken ct = default)
{
var response = await _httpClient.DeleteAsync($"{Endpoint}/{id}", ct);
var response = await httpClient.DeleteAsync($"{Endpoint}/{id}", ct);
if (response.IsSuccessStatusCode)
{
return ApiResult<bool>.Ok(true);

View File

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

View File

@@ -2,20 +2,14 @@ using DbFirst.Contracts.Layouts;
namespace DbFirst.BlazorWebApp.Services;
public class LayoutApiClient : ILayoutApiClient
public class LayoutApiClient(HttpClient httpClient) : ILayoutApiClient
{
private readonly HttpClient _httpClient;
private const string Endpoint = "api/layouts";
public LayoutApiClient(HttpClient httpClient)
{
_httpClient = httpClient;
}
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 response = await _httpClient.GetAsync(url, ct);
var response = await httpClient.GetAsync(url, ct);
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
{
return null;
@@ -27,7 +21,7 @@ public class LayoutApiClient : ILayoutApiClient
public async Task<LayoutDto> UpsertAsync(LayoutDto dto, CancellationToken ct = default)
{
var response = await _httpClient.PostAsJsonAsync(Endpoint, dto, ct);
var response = await httpClient.PostAsJsonAsync(Endpoint, dto, ct);
if (!response.IsSuccessStatusCode)
{
var detail = await ApiClientHelper.ReadErrorAsync(response);
@@ -41,7 +35,7 @@ public class LayoutApiClient : ILayoutApiClient
public async Task DeleteAsync(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 response = await _httpClient.DeleteAsync(url, ct);
var response = await httpClient.DeleteAsync(url, ct);
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
{
return;

View File

@@ -1,44 +1,33 @@
using DbFirst.BlazorWebApp.Models;
using DbFirst.Contracts.MassData;
using Microsoft.AspNetCore.WebUtilities;
namespace DbFirst.BlazorWebApp.Services;
public class MassDataApiClient : IMassDataApiClient
public class MassDataApiClient(HttpClient httpClient) : IMassDataApiClient
{
private readonly HttpClient _httpClient;
private const string Endpoint = "api/massdata";
public MassDataApiClient(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task<int> GetCountAsync(CancellationToken ct = default)
{
var result = await _httpClient.GetFromJsonAsync<int?>("api/massdata/count", ct);
var result = await httpClient.GetFromJsonAsync<int?>("api/massdata/count", ct);
return result ?? 0;
}
public async Task<List<MassDataReadDto>> GetAllAsync(int? skip, int? take, CancellationToken ct = default)
{
var query = new List<string>();
if (skip.HasValue)
{
query.Add($"skip={skip.Value}");
}
if (take.HasValue)
{
query.Add($"take={take.Value}");
}
var query = new Dictionary<string, string?>();
if (skip.HasValue) query["skip"] = skip.Value.ToString();
if (take.HasValue) query["take"] = take.Value.ToString();
var url = query.Count == 0 ? Endpoint : $"{Endpoint}?{string.Join("&", query)}";
var result = await _httpClient.GetFromJsonAsync<List<MassDataReadDto>>(url, ct);
var url = QueryHelpers.AddQueryString(Endpoint, query);
var result = await httpClient.GetFromJsonAsync<List<MassDataReadDto>>(url, ct);
return result ?? [];
}
public async Task<ApiResult<MassDataReadDto?>> UpsertAsync(MassDataWriteDto dto, CancellationToken ct = default)
{
var response = await _httpClient.PostAsJsonAsync($"{Endpoint}/upsert", dto, ct);
var response = await httpClient.PostAsJsonAsync($"{Endpoint}/upsert", dto, ct);
if (response.IsSuccessStatusCode)
{
var payload = await response.Content.ReadFromJsonAsync<MassDataReadDto>();
@@ -56,7 +45,7 @@ public class MassDataApiClient : IMassDataApiClient
return null;
}
var response = await _httpClient.GetAsync($"{Endpoint}/{Uri.EscapeDataString(customerName)}", ct);
var response = await httpClient.GetAsync($"{Endpoint}/{Uri.EscapeDataString(customerName)}", ct);
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
{
return null;