Compare commits

..

27 Commits

Author SHA1 Message Date
OlgunR
c5ca9f0048 Add grid font size selector and fix German character display
Introduced font size adjustment for grids via dropdown toolbar and CSS variable. Added JavaScript for dynamic font size changes. Replaced German umlauts with HTML entities for proper rendering. Refactored grid code and improved error/info message display. Enhances accessibility and user experience.
2026-02-19 17:01:33 +01:00
OlgunR
e7aa41aa4d Add dark mode toggle with ThemeState service
Implement dark mode support using a new ThemeState service that manages theme switching and integrates with DevExpress Blazor theming. Update App.razor to apply the theme globally, enhance MainLayout with a toggle button and dynamic CSS classes, and add dark mode styles to CSS files. Register ThemeState as a scoped service in Program.cs.
2026-02-18 10:07:14 +01:00
OlgunR
9a4f189e4e Add row focusing support to CatalogsGrid and MassDataGrid
Enable row focusing in both grid components by introducing the focusedRowKey property and binding it to the grid. After create or update operations, set focusedRowKey to the affected item's key, ensuring the grid automatically focuses the relevant row. Improves user experience by highlighting newly created or updated items.
2026-02-17 12:40:37 +01:00
OlgunR
797dd44f25 Remove BlazorWasm project and all related frontend assets
This commit deletes the entire Blazor WebAssembly frontend project, including all source code, components, DTOs, services, CSS, configuration files, static assets (such as favicon and icons), and the main HTML entry point. All references to `DbFirst.BlazorWasm` have been removed from the solution file, and associated dependencies like Bootstrap CSS and its source map are also deleted. This effectively removes the BlazorWasm application from the repository.
2026-02-11 08:15:47 +01:00
OlgunR
0e313b2e07 Remove custom grid sort/filter CSS from MassDataGrid
Deleted styles for custom sort icons, filter row layout, and search input in MassDataGrid. Retained only general layout and popup/editor styles, reverting to default grid visuals.
2026-02-10 13:36:08 +01:00
OlgunR
db232914a9 Switch grids to built-in filter menu, remove custom filters
Removed custom filter row templates and operator logic for date and number columns in CatalogsGrid.razor and MassDataGrid.razor. Grids now use DevExpress's built-in filter menu (FilterMenuButtonDisplayMode.Always), simplifying code and UI. All related filter fields and methods were deleted.
2026-02-10 13:32:21 +01:00
OlgunR
f49db97147 Remove DevExpress theme CSS links from App.razor
Removed references to fluent-light.bs5.min.css and bootstrap-external.bs5.min.css from App.razor. These DevExpress Blazor theme stylesheets are no longer included in the application. All other stylesheets remain unchanged.
2026-02-10 13:17:29 +01:00
OlgunR
0876ebe999 Update DevExpress Blazor to use Bootstrap 5 theme
Replaced Fluent theme CSS with Bootstrap 5 Fluent Light theme. Updated DevExpress Blazor service registration to explicitly use BootstrapVersion.v5 for consistent styling. Cleaned up and reordered CSS includes.
2026-02-10 09:54:39 +01:00
OlgunR
74504c583e Improve loading UX with spinner and wider filter fields
Replaced plain loading text with a centered Bootstrap spinner in CatalogsGrid and MassDataGrid. Introduced hasLoaded flag for more accurate loading state handling. Increased min-width of filter input fields for better usability. Added .loading-container CSS for consistent spinner placement.
2026-02-09 16:40:09 +01:00
OlgunR
8387b71676 Add operator selection to grid filter rows for dates/numbers
Introduce custom filter row cell templates for date and numeric columns in CatalogsGrid and MassDataGrid. Users can now select a filter operator (e.g., =, <, >, <=, >=) alongside the filter value for columns like AddedWhen, ChangedWhen, and Amount. Updated filter criteria logic to support these operators using DevExpress expressions. Added new CSS for filter row alignment and appearance. Removed obsolete methods and improved layout logic. This enhances filtering flexibility and user experience in both grids.
2026-02-09 16:10:16 +01:00
OlgunR
8824492057 Enable grouping features in DxGrid components
Added ShowGroupPanel, ShowGroupedColumns, and AllowGroup to DxGrid in CatalogsGrid.razor and MassDataGrid.razor for enhanced data grouping. Also set PagerVisible="false" in MassDataGrid.razor to hide pager controls.
2026-02-09 14:03:47 +01:00
OlgunR
59f22be405 Update nav menu with new custom SVG icons
Replaced icons for "Catalogs", "Dashboards", and "MassData" in NavMenu.razor with new custom Bootstrap-style SVG icons. Added corresponding CSS classes using data URIs to embed the new white icons, ensuring consistent appearance in the navigation menu.
2026-02-09 13:41:11 +01:00
OlgunR
23865aefb6 Remove Counter and Weather pages and nav links
Removed the Counter and Weather pages by deleting their component files. Also updated the navigation menu to remove links to these pages, leaving only Home and Catalogs. This simplifies the app's navigation and available features.
2026-02-09 13:22:04 +01:00
OlgunR
2a730ddfcc Refactor grid and band layout persistence logic
Unify band and grid layout saving into a single "Layout speichern" action and method. Store the full grid layout in BandLayout, and simplify column/band rendering logic. Apply saved layouts after render for consistency. Remove obsolete ordering logic and update messages for clarity. These changes improve robustness and maintainability of layout persistence in CatalogsGrid and MassDataGrid.
2026-02-09 11:36:56 +01:00
OlgunR
d78fd5e3d1 Add user-specific persistent grid/band layouts support
Implemented user-customizable, persistent grid and band layouts for CatalogsGrid and MassDataGrid. Added backend API, database entity, and repository for storing layouts per user. Refactored grids to support dynamic band/column rendering, layout management UI, and per-user storage via localStorage and the new API. Registered all necessary services and updated data context. Enables flexible, user-specific grid experiences with saved layouts.
2026-02-06 14:07:52 +01:00
OlgunR
52b2cf9a5b Add user-selectable page size to MassData grid
Users can now choose how many records to display per page in the MassData grid (100, 1,000, 10,000, 100,000, or all). The backend and API client are updated to support nullable skip/take parameters, allowing "all" records to be fetched when desired. The pager and page count calculations are updated to reflect the selected page size, and the pager is hidden if only one page exists. Additional UI and CSS changes provide a combo box for page size selection. The API controller now treats take <= 0 as "no limit."
2026-02-05 15:45:36 +01:00
OlgunR
9d7b3591cc Update grid column captions to German
Changed "Added" to "Angelegt am" and "Changed" to "Geändert am" in the data grid column headers for improved localization. No other column settings were modified.
2026-02-05 14:17:29 +01:00
OlgunR
006ee78422 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
2026-02-05 14:11:27 +01:00
OlgunR
a52d615750 Update grid popup headers and edit form logic
CatalogsGrid now sets popup edit form header dynamically ("Neu" for new, "Edit" for existing) and only shows the "Update-Prozedur" field when editing. Added IsNew property to CatalogEditModel. MassDataGrid sets popup header to "Bearbeiten". Also standardized DateChanged event handler style in both components.
2026-02-05 12:52:44 +01:00
OlgunR
9bbe34dece Enhance grid filter UI with custom editors for each type
Replaced default filter row editors with custom templates in CatalogsGrid and MassDataGrid. Added DxTextBox for text fields, DxDateEdit for date fields, and a DxComboBox for boolean status filtering. Introduced BoolFilterOption class to support the status dropdown. These changes improve filter usability and data type handling.
2026-02-05 11:37:06 +01:00
OlgunR
05ea47f42c Improve title field validation feedback in catalog edit form
Title field now validates on input, not just on blur. Added OnTitleChanged handler to clear validation messages for CatTitle as the user types. OnFieldChanged now only clears messages for UpdateProcedure. This enhances real-time validation feedback for users.
2026-02-05 11:21:49 +01:00
OlgunR
945c8aaf4a Refactor grids to use DxGrid popup editing
Modernize CatalogsGrid.razor and MassDataGrid.razor to use DevExpress DxGrid's built-in popup editing with EditFormTemplate. Remove custom EditForm panels and manual editing state logic. Move CRUD operations and validation to grid event handlers. Add field-level validation and error display for catalogs. Update grid columns, add command columns, and set audit fields to read-only. Only editing is allowed in MassDataGrid; deletion is disabled. Streamline code and UI for improved maintainability and user experience.
2026-02-05 10:43:40 +01:00
OlgunR
4ef80ce875 Improve MassDataGrid UI: custom sort icons & filter inputs
Replaced default sort icons with custom SVGs for clarity.
Updated filter row to use styled DxTextBox inputs with search icons.
Ensured consistent and intuitive filtering experience across columns.
2026-02-05 08:54:21 +01:00
OlgunR
88c34ef94b 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
2026-02-04 13:00:45 +01:00
OlgunR
85b9b0b51a Add MassData API with CQRS, repository, and DbContext
Introduce MassData feature with new API endpoints for querying and upserting records by customer name. Add DTOs, AutoMapper profile, MediatR CQRS handlers, repository pattern, and MassDataDbContext. Register new services in DI and add MassDataConnection to configuration. Upsert uses stored procedure. Enables full CRUD for Massdata via dedicated API.
2026-02-04 11:39:58 +01:00
OlgunR
013088a25f Add real-time dashboard updates with SignalR
Integrate SignalR to provide real-time dashboard update notifications.
- Added DashboardsHub and DashboardChangeNotifier on the backend.
- Modified SqlDashboardStorage to trigger notifications on changes.
- Registered SignalR services and mapped the hub endpoint.
- Updated Blazor clients to connect to the hub and refresh dashboards on change.
- Added SignalR client packages and necessary DI/configuration.
2026-02-04 09:01:28 +01:00
OlgunR
dbe1d9d206 Add mode param to dashboard links for designer/viewer mode
Dashboard navigation links now include a mode query parameter, set to "designer" or "viewer" based on the IsDesigner flag. This enables the app to distinguish between designer and viewer modes when navigating to dashboards.
2026-02-04 08:45:27 +01:00
77 changed files with 2474 additions and 1436 deletions

View File

@@ -0,0 +1,94 @@
using System.Text;
using DbFirst.Application.Repositories;
using DbFirst.Domain.Entities;
using Microsoft.AspNetCore.Mvc;
namespace DbFirst.API.Controllers;
[ApiController]
[Route("api/[controller]")]
public class LayoutsController : ControllerBase
{
private readonly ILayoutRepository _repository;
public LayoutsController(ILayoutRepository repository)
{
_repository = repository;
}
[HttpGet]
public async Task<ActionResult<LayoutDto>> Get([FromQuery] string layoutType, [FromQuery] string layoutKey, [FromQuery] string userName, CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(layoutType) || string.IsNullOrWhiteSpace(layoutKey) || string.IsNullOrWhiteSpace(userName))
{
return BadRequest("layoutType, layoutKey und userName sind erforderlich.");
}
var entity = await _repository.GetAsync(layoutType, layoutKey, userName, cancellationToken);
if (entity == null)
{
return NotFound();
}
return Ok(Map(entity));
}
[HttpPost]
public async Task<ActionResult<LayoutDto>> Upsert(LayoutDto dto, CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(dto.LayoutType) || string.IsNullOrWhiteSpace(dto.LayoutKey) || string.IsNullOrWhiteSpace(dto.UserName))
{
return BadRequest("LayoutType, LayoutKey und UserName sind erforderlich.");
}
var data = string.IsNullOrWhiteSpace(dto.LayoutData)
? Array.Empty<byte>()
: Encoding.UTF8.GetBytes(dto.LayoutData);
try
{
var entity = await _repository.UpsertAsync(dto.LayoutType, dto.LayoutKey, dto.UserName, data, cancellationToken);
return Ok(Map(entity));
}
catch (Exception ex)
{
var detail = ex.InnerException?.Message ?? ex.Message;
return Problem(detail: detail, statusCode: StatusCodes.Status500InternalServerError);
}
}
[HttpDelete]
public async Task<IActionResult> Delete([FromQuery] string layoutType, [FromQuery] string layoutKey, [FromQuery] string userName, CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(layoutType) || string.IsNullOrWhiteSpace(layoutKey) || string.IsNullOrWhiteSpace(userName))
{
return BadRequest("layoutType, layoutKey und userName sind erforderlich.");
}
var deleted = await _repository.DeleteAsync(layoutType, layoutKey, userName, cancellationToken);
return deleted ? NoContent() : NotFound();
}
private static LayoutDto Map(SmfLayout entity)
{
var layoutData = entity.LayoutData.Length == 0
? string.Empty
: Encoding.UTF8.GetString(entity.LayoutData);
return new LayoutDto
{
LayoutType = entity.LayoutType,
LayoutKey = entity.LayoutKey,
UserName = entity.UserName,
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

@@ -0,0 +1,58 @@
using DbFirst.Application.MassData;
using DbFirst.Application.MassData.Commands;
using DbFirst.Application.MassData.Queries;
using MediatR;
using Microsoft.AspNetCore.Mvc;
namespace DbFirst.API.Controllers;
[ApiController]
[Route("api/[controller]")]
public class MassDataController : ControllerBase
{
private readonly IMediator _mediator;
public MassDataController(IMediator mediator)
{
_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)
{
int? resolvedTake = take;
if (resolvedTake is <= 0)
{
resolvedTake = null;
}
var result = await _mediator.Send(new GetAllMassDataQuery(skip, resolvedTake), cancellationToken);
return Ok(result);
}
[HttpGet("{customerName}")]
public async Task<ActionResult<MassDataReadDto>> GetByCustomerName(string customerName, CancellationToken cancellationToken)
{
var result = await _mediator.Send(new GetMassDataByCustomerNameQuery(customerName), cancellationToken);
if (result == null)
{
return NotFound();
}
return Ok(result);
}
[HttpPost("upsert")]
public async Task<ActionResult<MassDataReadDto>> Upsert(MassDataWriteDto dto, CancellationToken cancellationToken)
{
var result = await _mediator.Send(new UpsertMassDataByCustomerNameCommand(dto), cancellationToken);
return Ok(result);
}
}

View File

@@ -0,0 +1,19 @@
using DbFirst.API.Hubs;
using Microsoft.AspNetCore.SignalR;
namespace DbFirst.API.Dashboards;
public class DashboardChangeNotifier : IDashboardChangeNotifier
{
private readonly IHubContext<DashboardsHub> _hubContext;
public DashboardChangeNotifier(IHubContext<DashboardsHub> hubContext)
{
_hubContext = hubContext;
}
public void NotifyChanged()
{
_ = _hubContext.Clients.All.SendAsync("DashboardsChanged");
}
}

View File

@@ -0,0 +1,6 @@
namespace DbFirst.API.Dashboards;
public interface IDashboardChangeNotifier
{
void NotifyChanged();
}

View File

@@ -11,12 +11,14 @@ public sealed class SqlDashboardStorage : IEditableDashboardStorage
private readonly string _connectionString; private readonly string _connectionString;
private readonly string _tableName; private readonly string _tableName;
private readonly Func<string?>? _userProvider; private readonly Func<string?>? _userProvider;
private readonly IDashboardChangeNotifier? _notifier;
public SqlDashboardStorage(string connectionString, string tableName = "TBDD_SMF_CONFIG", Func<string?>? userProvider = null) public SqlDashboardStorage(string connectionString, string tableName = "TBDD_SMF_CONFIG", Func<string?>? userProvider = null, IDashboardChangeNotifier? notifier = null)
{ {
_connectionString = connectionString; _connectionString = connectionString;
_tableName = tableName; _tableName = tableName;
_userProvider = userProvider; _userProvider = userProvider;
_notifier = notifier;
} }
public IEnumerable<DashboardInfo> GetAvailableDashboardsInfo() public IEnumerable<DashboardInfo> GetAvailableDashboardsInfo()
@@ -98,6 +100,7 @@ public sealed class SqlDashboardStorage : IEditableDashboardStorage
connection.Open(); connection.Open();
command.ExecuteNonQuery(); command.ExecuteNonQuery();
_notifier?.NotifyChanged();
return id; return id;
} }
@@ -118,6 +121,8 @@ public sealed class SqlDashboardStorage : IEditableDashboardStorage
{ {
throw new ArgumentException($"Dashboard '{dashboardId}' not found."); throw new ArgumentException($"Dashboard '{dashboardId}' not found.");
} }
_notifier?.NotifyChanged();
} }
public void DeleteDashboard(string dashboardId) public void DeleteDashboard(string dashboardId)
@@ -128,5 +133,6 @@ public sealed class SqlDashboardStorage : IEditableDashboardStorage
connection.Open(); connection.Open();
command.ExecuteNonQuery(); command.ExecuteNonQuery();
_notifier?.NotifyChanged();
} }
} }

View File

@@ -0,0 +1,7 @@
using Microsoft.AspNetCore.SignalR;
namespace DbFirst.API.Hubs;
public class DashboardsHub : Hub
{
}

View File

@@ -1,5 +1,6 @@
using DbFirst.API.Middleware; using DbFirst.API.Middleware;
using DbFirst.API.Dashboards; using DbFirst.API.Dashboards;
using DbFirst.API.Hubs;
using DbFirst.Application; using DbFirst.Application;
using DbFirst.Application.Repositories; using DbFirst.Application.Repositories;
using DbFirst.Domain; using DbFirst.Domain;
@@ -51,8 +52,12 @@ builder.Services.AddInfrastructure(builder.Configuration);
builder.Services.AddApplication(); builder.Services.AddApplication();
builder.Services.AddScoped<ICatalogRepository, CatalogRepository>(); 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.AddSingleton<IDashboardChangeNotifier, DashboardChangeNotifier>();
builder.Services.AddScoped<DashboardConfigurator>((IServiceProvider serviceProvider) => { builder.Services.AddScoped<DashboardConfigurator>((IServiceProvider serviceProvider) => {
var dashboardsPath = Path.Combine(builder.Environment.ContentRootPath, "Data", "Dashboards"); var dashboardsPath = Path.Combine(builder.Environment.ContentRootPath, "Data", "Dashboards");
Directory.CreateDirectory(dashboardsPath); Directory.CreateDirectory(dashboardsPath);
@@ -112,7 +117,8 @@ builder.Services.AddScoped<DashboardConfigurator>((IServiceProvider serviceProvi
DashboardConfigurator configurator = new DashboardConfigurator(); DashboardConfigurator configurator = new DashboardConfigurator();
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") ?? string.Empty; var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") ?? string.Empty;
var dashboardStorage = new SqlDashboardStorage(connectionString, "TBDD_SMF_CONFIG"); var notifier = serviceProvider.GetRequiredService<IDashboardChangeNotifier>();
var dashboardStorage = new SqlDashboardStorage(connectionString, "TBDD_SMF_CONFIG", notifier: notifier);
configurator.SetDashboardStorage(dashboardStorage); configurator.SetDashboardStorage(dashboardStorage);
DataSourceInMemoryStorage dataSourceStorage = new DataSourceInMemoryStorage(); DataSourceInMemoryStorage dataSourceStorage = new DataSourceInMemoryStorage();
@@ -155,7 +161,7 @@ app.UseCors();
app.UseAuthorization(); app.UseAuthorization();
app.MapDashboardRoute("api/dashboard", "DefaultDashboard"); app.MapDashboardRoute("api/dashboard", "DefaultDashboard");
app.MapHub<DashboardsHub>("/hubs/dashboards");
app.MapControllers(); app.MapControllers();
app.Run(); app.Run();

View File

@@ -1,6 +1,7 @@
{ {
"ConnectionStrings": { "ConnectionStrings": {
"DefaultConnection": "Server=SDD-VMP04-SQL17\\DD_DEVELOP01;Database=DD_ECM;User Id=sa;Password=dd;TrustServerCertificate=True;" "DefaultConnection": "Server=SDD-VMP04-SQL17\\DD_DEVELOP01;Database=DD_ECM;User Id=sa;Password=dd;TrustServerCertificate=True;",
"MassDataConnection": "Server=SDD-VMP04-SQL19\\DD_DEVELOP01;Database=DD_ECM;User Id=sa;Password=dd;TrustServerCertificate=True;"
}, },
"Dashboard": { "Dashboard": {
"BaseUrl": "https://localhost:7204" "BaseUrl": "https://localhost:7204"

View File

@@ -0,0 +1,5 @@
using MediatR;
namespace DbFirst.Application.MassData.Commands;
public record UpsertMassDataByCustomerNameCommand(MassDataWriteDto Dto) : IRequest<MassDataReadDto>;

View File

@@ -0,0 +1,24 @@
using AutoMapper;
using DbFirst.Application.Repositories;
using MediatR;
namespace DbFirst.Application.MassData.Commands;
public class UpsertMassDataByCustomerNameHandler : IRequestHandler<UpsertMassDataByCustomerNameCommand, MassDataReadDto>
{
private readonly IMassDataRepository _repository;
private readonly IMapper _mapper;
public UpsertMassDataByCustomerNameHandler(IMassDataRepository repository, IMapper mapper)
{
_repository = repository;
_mapper = mapper;
}
public async Task<MassDataReadDto> Handle(UpsertMassDataByCustomerNameCommand request, CancellationToken cancellationToken)
{
var dto = request.Dto;
var updated = await _repository.UpsertByCustomerNameAsync(dto.CustomerName, dto.Amount, dto.StatusFlag, dto.Category, cancellationToken);
return _mapper.Map<MassDataReadDto>(updated);
}
}

View File

@@ -0,0 +1,12 @@
using AutoMapper;
using DbFirst.Domain.Entities;
namespace DbFirst.Application.MassData;
public class MassDataProfile : Profile
{
public MassDataProfile()
{
CreateMap<Massdata, MassDataReadDto>();
}
}

View File

@@ -0,0 +1,12 @@
namespace DbFirst.Application.MassData;
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.Application.MassData;
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,23 @@
using AutoMapper;
using DbFirst.Application.Repositories;
using MediatR;
namespace DbFirst.Application.MassData.Queries;
public class GetAllMassDataHandler : IRequestHandler<GetAllMassDataQuery, List<MassDataReadDto>>
{
private readonly IMassDataRepository _repository;
private readonly IMapper _mapper;
public GetAllMassDataHandler(IMassDataRepository repository, IMapper mapper)
{
_repository = repository;
_mapper = mapper;
}
public async Task<List<MassDataReadDto>> Handle(GetAllMassDataQuery request, CancellationToken cancellationToken)
{
var items = await _repository.GetAllAsync(request.Skip, request.Take, cancellationToken);
return _mapper.Map<List<MassDataReadDto>>(items);
}
}

View File

@@ -0,0 +1,5 @@
using MediatR;
namespace DbFirst.Application.MassData.Queries;
public record GetAllMassDataQuery(int? Skip, int? Take) : IRequest<List<MassDataReadDto>>;

View File

@@ -0,0 +1,23 @@
using AutoMapper;
using DbFirst.Application.Repositories;
using MediatR;
namespace DbFirst.Application.MassData.Queries;
public class GetMassDataByCustomerNameHandler : IRequestHandler<GetMassDataByCustomerNameQuery, MassDataReadDto?>
{
private readonly IMassDataRepository _repository;
private readonly IMapper _mapper;
public GetMassDataByCustomerNameHandler(IMassDataRepository repository, IMapper mapper)
{
_repository = repository;
_mapper = mapper;
}
public async Task<MassDataReadDto?> Handle(GetMassDataByCustomerNameQuery request, CancellationToken cancellationToken)
{
var item = await _repository.GetByCustomerNameAsync(request.CustomerName, cancellationToken);
return item == null ? null : _mapper.Map<MassDataReadDto>(item);
}
}

View File

@@ -0,0 +1,5 @@
using MediatR;
namespace DbFirst.Application.MassData.Queries;
public record GetMassDataByCustomerNameQuery(string CustomerName) : IRequest<MassDataReadDto?>;

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

@@ -0,0 +1,10 @@
using DbFirst.Domain.Entities;
namespace DbFirst.Application.Repositories;
public interface ILayoutRepository
{
Task<SmfLayout?> GetAsync(string layoutType, string layoutKey, string userName, CancellationToken cancellationToken = default);
Task<SmfLayout> UpsertAsync(string layoutType, string layoutKey, string userName, byte[] layoutData, CancellationToken cancellationToken = default);
Task<bool> DeleteAsync(string layoutType, string layoutKey, string userName, CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,11 @@
using DbFirst.Domain.Entities;
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

@@ -1,60 +0,0 @@
/*
Ablauf und die Rolle jeder Datei in der Blazor WebAssembly-Anwendung:
1.index.html
• Reihenfolge: Wird als erstes geladen, wenn die Anwendung im Browser geöffnet wird.
• Purpose:
• Lädt die Blazor WebAssembly-Umgebung (blazor.webassembly.js).
• Definiert den Platzhalter <div id="app">, in dem die Blazor-Komponenten gerendert werden.
• Bindet Stylesheets und Skripte ein, die für das Styling und die Funktionalität der Anwendung benötigt werden.
2. Program.cs
• Reihenfolge: Wird nach index.html ausgeführt, sobald die Blazor-Umgebung initialisiert ist.
• Purpose:
• Initialisiert die Blazor WebAssembly-Anwendung.
• Registriert Root-Komponenten (App) und Abhängigkeiten (z. B. HttpClient, CatalogApiClient).
• Konfiguriert die Basis-URL für API-Aufrufe.
3. App.razor
• Reihenfolge: Wird als nächstes geladen, nachdem die Anwendung initialisiert wurde.
• Purpose:
• Definiert die Routing-Logik der Anwendung.
• Entscheidet, welche Komponente basierend auf der URL gerendert wird.
• Stellt sicher, dass ein Standardlayout (MainLayout) verwendet wird.
4.MainLayout.razor
• Reihenfolge: Wird geladen, wenn eine Seite gerendert wird, da es das Standardlayout ist.
• Purpose:
• Definiert das Hauptlayout der Anwendung.
• Enthält die Navigationsleiste (NavMenu) und den Platzhalter für den Seiteninhalt (@Body).
5. NavMenu.razor
• Reihenfolge: Wird als Teil des Layouts (MainLayout) geladen.
• Purpose:
• Stellt die Navigationsleiste bereit.
• Enthält Links zu verschiedenen Seiten der Anwendung (z. B. Home, Catalogs).
• Ermöglicht das Ein- und Ausklappen des Menüs.
6. Catalogs.razor
• Reihenfolge: Wird geladen, wenn der Benutzer die URL /catalogs aufruft.
• Purpose:
• Stellt die Benutzeroberfläche für die Verwaltung von Katalogen bereit.
• Nutzt CatalogApiClient, um Daten von der API zu laden, zu erstellen, zu aktualisieren oder zu löschen.
• Verwendet DevExpress-Komponenten für ein modernes UI.
7. CatalogApiClient.cs
• Reihenfolge: Wird verwendet, wenn Catalogs.razor API-Aufrufe ausführt.
• Purpose:
• Kapselt die Kommunikation mit der API.
• Bietet Methoden für CRUD-Operationen (Create, Read, Update, Delete) auf Katalog-Daten.
• Behandelt Fehler und gibt benutzerfreundliche Fehlermeldungen zurück.
Zusammenfassung des Ablaufs:
1.index.html: Lädt die Blazor-Umgebung und startet die Anwendung.
2. Program.cs: Initialisiert die Anwendung und registriert Abhängigkeiten.
3. App.razor: Definiert die Routing-Logik und lädt das Standardlayout.
4. MainLayout.razor: Stellt das Hauptlayout bereit.
5. NavMenu.razor: Lädt die Navigationsleiste.
6. Seiten wie Catalogs.razor: Werden basierend auf der URL gerendert.
7. CatalogApiClient.cs: Führt API-Aufrufe aus, wenn die Seite Daten benötigt.
*/

View File

@@ -1,20 +0,0 @@
@*
• Ist der logische Einstiegspunkt der Blazor-Anwendung.
• Sie definiert die Routing-Logik und das Standardlayout der Anwendung.
• Der Router-Komponent in App.razor entscheidet, welche Blazor-Komponente basierend auf der URL geladen wird.
kurz: Steuert die Navigation und das Rendering der Blazor-Komponenten.
*@
@DxResourceManager.RegisterTheme(Themes.Fluent)
@DxResourceManager.RegisterScripts()
<Router AppAssembly="@typeof(App).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
</Found>
<NotFound>
<PageTitle>Not found</PageTitle>
<LayoutView Layout="@typeof(MainLayout)">
<p role="alert">Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>

View File

@@ -1,296 +0,0 @@
@inject CatalogApiClient Api
<style>
.action-panel { margin-bottom: 16px; }
.grid-section { margin-top: 12px; }
.catalog-grid .dxbl-grid-sort-asc,
.catalog-grid .dxbl-grid-sort-desc {
display: none;
}
.catalog-grid th.dxbl-grid-header-sortable {
position: relative;
padding-right: 1.5rem;
}
.catalog-grid th.dxbl-grid-header-sortable::before,
.catalog-grid th.dxbl-grid-header-sortable::after {
content: "";
position: absolute;
right: 0.45rem;
width: 0.7rem;
height: 0.7rem;
background-repeat: no-repeat;
background-size: 0.7rem 0.7rem;
opacity: 0.35;
pointer-events: none;
}
.catalog-grid th.dxbl-grid-header-sortable::before {
top: 38%;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M4.957 10.999a1 1 0 0 1-.821-1.571l2.633-3.785a1.5 1.5 0 0 1 2.462 0l2.633 3.785a1 1 0 0 1-.821 1.57H4.957Z' fill='%23888888'/%3E%3C/svg%3E");
}
.catalog-grid th.dxbl-grid-header-sortable::after {
top: 58%;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M4.957 5a1 1 0 0 0-.821 1.571l2.633 3.784a1.5 1.5 0 0 0 2.462 0l2.633-3.784A1 1 0 0 0 11.043 5H4.957Z' fill='%23888888'/%3E%3C/svg%3E");
}
.catalog-grid th.dxbl-grid-header-sortable[aria-sort="ascending"]::after {
opacity: 0;
}
.catalog-grid th.dxbl-grid-header-sortable[aria-sort="descending"]::before {
opacity: 0;
}
.catalog-grid .filter-search-input input {
padding-right: 1.75rem;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M9.309 10.016a4.5 4.5 0 1 1 .707-.707l3.838 3.837a.5.5 0 0 1-.708.708L9.31 10.016ZM10 6.5a3.5 3.5 0 1 0-7 0 3.5 3.5 0 0 0 7 0Z' fill='%23666666'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 0.5rem center;
background-size: 0.9rem;
}
</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="Titel" Context="itemCtx">
<DxTextBox @bind-Text="formModel.CatTitle" Enabled="@(isEditing ? formModel.UpdateProcedure != 0 : true)" />
</DxFormLayoutItem>
<DxFormLayoutItem Caption="Kennung" Context="itemCtx">
<DxTextBox @bind-Text="formModel.CatString" />
</DxFormLayoutItem>
@if (isEditing)
{
<DxFormLayoutItem Caption="Update-Prozedur" Context="itemCtx">
<DxComboBox Data="@procedureOptions"
TextFieldName="Text"
ValueFieldName="Value"
@bind-Value="formModel.UpdateProcedure" />
</DxFormLayoutItem>
}
<DxFormLayoutItem Caption=" " Context="itemCtx">
<DxStack Orientation="Orientation.Horizontal" Spacing="8">
<DxButton RenderStyle="ButtonRenderStyle.Success" ButtonType="ButtonType.Submit" SubmitFormOnClick="true" Context="btnCtx">@((isEditing ? "Speichern" : "Anlegen"))</DxButton>
<DxButton RenderStyle="ButtonRenderStyle.Secondary" Click="@CancelEdit" Context="btnCtx">Abbrechen</DxButton>
</DxStack>
</DxFormLayoutItem>
</DxFormLayout>
</EditForm>
</div>
}
@if (isLoading)
{
<p><em>Lade Daten...</em></p>
}
else if (items.Count == 0)
{
<p>Keine Einträge vorhanden.</p>
}
else
{
<div class="grid-section">
<DxGrid Data="@items" TItem="CatalogReadDto" KeyFieldName="@nameof(CatalogReadDto.Guid)" ShowFilterRow="true" PageSize="10" CssClass="mb-4 catalog-grid">
<Columns>
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.Guid)" Caption="Id" Width="140px" SortIndex="0" SortOrder="GridColumnSortOrder.Ascending">
<FilterRowCellTemplate Context="filter">
<DxTextBox Text="@(filter.FilterRowValue?.ToString())"
TextChanged="@(value => filter.FilterRowValue = value)"
CssClass="filter-search-input" />
</FilterRowCellTemplate>
</DxGridDataColumn>
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.CatTitle)" Caption="Titel">
<FilterRowCellTemplate Context="filter">
<DxTextBox Text="@(filter.FilterRowValue as string)"
TextChanged="@(value => filter.FilterRowValue = value)"
CssClass="filter-search-input" />
</FilterRowCellTemplate>
</DxGridDataColumn>
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.CatString)" Caption="String">
<FilterRowCellTemplate Context="filter">
<DxTextBox Text="@(filter.FilterRowValue as string)"
TextChanged="@(value => filter.FilterRowValue = value)"
CssClass="filter-search-input" />
</FilterRowCellTemplate>
</DxGridDataColumn>
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.AddedWho)" Caption="Angelegt von">
<FilterRowCellTemplate Context="filter">
<DxTextBox Text="@(filter.FilterRowValue as string)"
TextChanged="@(value => filter.FilterRowValue = value)"
CssClass="filter-search-input" />
</FilterRowCellTemplate>
</DxGridDataColumn>
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.AddedWhen)" Caption="Angelegt am" />
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.ChangedWho)" Caption="Geändert von">
<FilterRowCellTemplate Context="filter">
<DxTextBox Text="@(filter.FilterRowValue as string)"
TextChanged="@(value => filter.FilterRowValue = value)"
CssClass="filter-search-input" />
</FilterRowCellTemplate>
</DxGridDataColumn>
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.ChangedWhen)" Caption="Geändert am" />
<DxGridDataColumn Caption="" Width="220px" AllowSort="false">
<CellDisplayTemplate Context="cell">
@{ var item = (CatalogReadDto)cell.DataItem; }
<div style="white-space: nowrap;">
<DxButton RenderStyle="ButtonRenderStyle.Secondary" Size="ButtonSize.Small" Click="@(() => StartEdit(item))">Bearbeiten</DxButton>
<DxButton RenderStyle="ButtonRenderStyle.Danger" Size="ButtonSize.Small" Click="@(() => DeleteCatalog(item.Guid))">Löschen</DxButton>
</div>
</CellDisplayTemplate>
</DxGridDataColumn>
</Columns>
</DxGrid>
</div>
}
@code {
private List<CatalogReadDto> items = new();
private CatalogWriteDto formModel = new();
private int editingId;
private bool isLoading;
private bool isEditing;
private bool showForm;
private string? errorMessage;
private string? infoMessage;
private readonly List<ProcedureOption> procedureOptions = new()
{
new() { Value = 0, Text = "PRTBMY_CATALOG_UPDATE" },
new() { Value = 1, Text = "PRTBMY_CATALOG_SAVE" }
};
protected override async Task OnInitializedAsync()
{
await LoadCatalogs();
}
private async Task LoadCatalogs()
{
isLoading = true;
errorMessage = null;
try
{
items = await Api.GetAllAsync();
}
catch (Exception ex)
{
errorMessage = $"Kataloge konnten nicht geladen werden: {ex.Message}";
}
finally
{
isLoading = false;
StateHasChanged();
}
}
private void StartCreate()
{
formModel = new CatalogWriteDto();
editingId = 0;
isEditing = false;
showForm = true;
infoMessage = null;
errorMessage = null;
}
private void StartEdit(CatalogReadDto item)
{
formModel = new CatalogWriteDto
{
CatTitle = item.CatTitle,
CatString = item.CatString,
UpdateProcedure = 0
};
editingId = item.Guid;
isEditing = true;
showForm = true;
infoMessage = null;
errorMessage = null;
}
private async Task HandleSubmit()
{
errorMessage = null;
infoMessage = null;
try
{
if (isEditing)
{
var updated = await Api.UpdateAsync(editingId, formModel);
if (!updated.Success)
{
errorMessage = updated.Error ?? "Aktualisierung fehlgeschlagen.";
return;
}
infoMessage = "Katalog aktualisiert.";
}
else
{
var created = await Api.CreateAsync(formModel);
if (!created.Success || created.Value == null)
{
errorMessage = created.Error ?? "Anlegen fehlgeschlagen.";
return;
}
infoMessage = "Katalog angelegt.";
}
showForm = false;
await LoadCatalogs();
}
catch (Exception ex)
{
errorMessage = $"Fehler beim Speichern: {ex.Message}";
}
}
private void CancelEdit()
{
showForm = false;
infoMessage = null;
errorMessage = null;
}
private async Task DeleteCatalog(int id)
{
errorMessage = null;
infoMessage = null;
try
{
var deleted = await Api.DeleteAsync(id);
if (!deleted.Success)
{
errorMessage = deleted.Error ?? "Löschen fehlgeschlagen.";
return;
}
infoMessage = "Katalog gelöscht.";
await LoadCatalogs();
}
catch (Exception ex)
{
errorMessage = $"Fehler beim Löschen: {ex.Message}";
}
}
private sealed class ProcedureOption
{
public int Value { get; set; }
public string Text { get; set; } = string.Empty;
}
}

View File

@@ -1,22 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DevExpress.Blazor" Version="25.2.3" />
<PackageReference Include="DevExpress.Blazor.Dashboard" Version="25.2.3" />
<PackageReference Include="DevExpress.Blazor.Themes" Version="25.2.3" />
<PackageReference Include="DevExpress.Blazor.Themes.Fluent" Version="25.2.3" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.22" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.22" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
<Folder Include="wwwroot\sample-data\" />
</ItemGroup>
</Project>

View File

@@ -1,19 +0,0 @@
@* Definiert das Hauptlayout der Anwendung.
Enthält die Navigationsleiste und den Hauptinhalt. *@
@inherits LayoutComponentBase
<div class="page">
<div class="sidebar">
<NavMenu /> <!-- Einbindung der Navigationsleiste -->
</div>
<main>
<div class="top-row px-4">
<a href="https://learn.microsoft.com/aspnet/core/" target="_blank">About</a>
</div>
<article class="content px-4">
@Body <!-- Platzhalter für den Seiteninhalt -->
</article>
</main>
</div>

View File

@@ -1,77 +0,0 @@
.page {
position: relative;
display: flex;
flex-direction: column;
}
main {
flex: 1;
}
.sidebar {
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
}
.top-row {
background-color: #f7f7f7;
border-bottom: 1px solid #d6d5d5;
justify-content: flex-end;
height: 3.5rem;
display: flex;
align-items: center;
}
.top-row ::deep a, .top-row ::deep .btn-link {
white-space: nowrap;
margin-left: 1.5rem;
text-decoration: none;
}
.top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
text-decoration: underline;
}
.top-row ::deep a:first-child {
overflow: hidden;
text-overflow: ellipsis;
}
@media (max-width: 640.98px) {
.top-row {
justify-content: space-between;
}
.top-row ::deep a, .top-row ::deep .btn-link {
margin-left: 0;
}
}
@media (min-width: 641px) {
.page {
flex-direction: row;
}
.sidebar {
width: 250px;
height: 100vh;
position: sticky;
top: 0;
}
.top-row {
position: sticky;
top: 0;
z-index: 1;
}
.top-row.auth ::deep a:first-child {
flex: 1;
text-align: right;
width: 0;
}
.top-row, article {
padding-left: 2rem !important;
padding-right: 1.5rem !important;
}
}

View File

@@ -1,43 +0,0 @@
@* Definiert die Navigationsleiste, die Links zu verschiedenen Seiten der Anwendung enthält. *@
<div class="top-row ps-3 navbar navbar-dark">
<div class="container-fluid">
<a class="navbar-brand" href="">DbFirst.BlazorWasm</a>
<button title="Navigation menu" class="navbar-toggler" @onclick="ToggleNavMenu">
<span class="navbar-toggler-icon"></span>
</button>
</div>
</div>
<div class="@NavMenuCssClass nav-scrollable" @onclick="ToggleNavMenu">
<nav class="flex-column">
<div class="nav-item px-3">
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
<span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="catalogs">
<span class="bi bi-collection-nav-menu" aria-hidden="true"></span> Catalogs
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="dashboards">
<span class="oi oi-list-rich" aria-hidden="true"></span> Dashboards
</NavLink>
</div>
</nav>
</div>
@code {
private bool collapseNavMenu = true;
// CSS-Klasse für die Navigation, die den Zustand (eingeklappt/ausgeklappt) steuert.
private string? NavMenuCssClass => collapseNavMenu ? "collapse" : null;
// Methode zum Umschalten des Navigationsmenüs.
private void ToggleNavMenu()
{
collapseNavMenu = !collapseNavMenu;
}
}

View File

@@ -1,83 +0,0 @@
.navbar-toggler {
background-color: rgba(255, 255, 255, 0.1);
}
.top-row {
height: 3.5rem;
background-color: rgba(0,0,0,0.4);
}
.navbar-brand {
font-size: 1.1rem;
}
.bi {
display: inline-block;
position: relative;
width: 1.25rem;
height: 1.25rem;
margin-right: 0.75rem;
top: -1px;
background-size: cover;
}
.bi-house-door-fill-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E");
}
.bi-plus-square-fill-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E");
}
.bi-list-nested-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E");
}
.nav-item {
font-size: 0.9rem;
padding-bottom: 0.5rem;
}
.nav-item:first-of-type {
padding-top: 1rem;
}
.nav-item:last-of-type {
padding-bottom: 1rem;
}
.nav-item ::deep a {
color: #d7d7d7;
border-radius: 4px;
height: 3rem;
display: flex;
align-items: center;
line-height: 3rem;
}
.nav-item ::deep a.active {
background-color: rgba(255,255,255,0.37);
color: white;
}
.nav-item ::deep a:hover {
background-color: rgba(255,255,255,0.1);
color: white;
}
@media (min-width: 641px) {
.navbar-toggler {
display: none;
}
.collapse {
/* Never collapse the sidebar for wide screens */
display: block;
}
.nav-scrollable {
/* Allow sidebar to scroll for tall menus */
height: calc(100vh - 3.5rem);
overflow-y: auto;
}
}

View File

@@ -1,12 +0,0 @@
namespace DbFirst.BlazorWasm.Models;
public class CatalogReadDto
{
public int Guid { get; set; }
public string CatTitle { get; set; } = string.Empty;
public string CatString { get; set; } = string.Empty;
public string AddedWho { get; set; } = string.Empty;
public DateTime AddedWhen { get; set; }
public string? ChangedWho { get; set; }
public DateTime? ChangedWhen { get; set; }
}

View File

@@ -1,8 +0,0 @@
namespace DbFirst.BlazorWasm.Models;
public class CatalogWriteDto
{
public string CatTitle { get; set; } = string.Empty;
public string CatString { get; set; } = string.Empty;
public int UpdateProcedure { get; set; } = 0; // 0 = Update, 1 = Save
}

View File

@@ -1,7 +0,0 @@
namespace DbFirst.BlazorWasm.Models;
public class DashboardInfoDto
{
public string Id { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
}

View File

@@ -1,7 +0,0 @@
@page "/catalogs"
<PageTitle>Catalogs</PageTitle>
<h1>Catalogs</h1>
<CatalogsGrid />

View File

@@ -1,122 +0,0 @@
@page "/dashboard"
@page "/dashboards/{DashboardId?}"
@inject Microsoft.Extensions.Configuration.IConfiguration Configuration
@inject NavigationManager Navigation
@inject DashboardApiClient DashboardApi
<style>
.dashboard-shell {
display: flex;
gap: 0;
min-height: 800px;
border: 1px solid #e6e6e6;
border-radius: 6px;
overflow: hidden;
background: #fff;
}
.dashboard-nav {
width: 220px;
border-right: 1px solid #e6e6e6;
background: #fafafa;
}
.dashboard-nav-title {
padding: 0.75rem 1rem 0.5rem;
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: #6c757d;
font-weight: 600;
}
.dashboard-nav-link {
display: block;
padding: 0.55rem 1rem;
color: inherit;
text-decoration: none;
}
.dashboard-nav-link.active {
background: #e9ecef;
font-weight: 600;
}
.dashboard-content {
flex: 1;
min-width: 0;
padding: 1rem;
}
</style>
<PageTitle>Dashboards</PageTitle>
<div class="dashboard-shell">
<aside class="dashboard-nav">
<div class="dashboard-nav-title">Dashboards</div>
@if (dashboards.Count == 0)
{
<div class="px-3 py-2 text-muted">Keine Dashboards vorhanden.</div>
}
else
{
@foreach (var dashboard in dashboards)
{
<NavLink class="dashboard-nav-link" href="@($"dashboards/{dashboard.Id}")">@dashboard.Name</NavLink>
}
}
</aside>
<section class="dashboard-content">
<div class="mb-3">
<DxButton RenderStyle="ButtonRenderStyle.Primary" Click="@ToggleMode">
@(IsDesigner ? "Zum Viewer wechseln" : "Zum Designer wechseln")
</DxButton>
</div>
<DxDashboard @key="DashboardKey" Endpoint="@DashboardEndpoint" InitialDashboardId="@SelectedDashboardId" WorkingMode="@CurrentMode" style="width: 100%; height: 800px;">
</DxDashboard>
</section>
</div>
@code {
[Parameter] public string? DashboardId { get; set; }
[SupplyParameterFromQuery] public string? Mode { get; set; }
private readonly List<DashboardInfoDto> dashboards = new();
private bool IsDesigner => !string.Equals(Mode, "viewer", StringComparison.OrdinalIgnoreCase);
private WorkingMode CurrentMode => IsDesigner ? WorkingMode.Designer : WorkingMode.ViewerOnly;
private string SelectedDashboardId { get; set; } = "";
private string DashboardKey => $"{SelectedDashboardId}-{(IsDesigner ? "designer" : "viewer")}";
private string DashboardEndpoint => $"{Configuration["ApiBaseUrl"]?.TrimEnd('/')}/api/dashboard";
protected override async Task OnParametersSetAsync()
{
if (dashboards.Count == 0)
{
dashboards.AddRange(await DashboardApi.GetAllAsync());
}
var requestedId = string.IsNullOrWhiteSpace(DashboardId) || string.Equals(DashboardId, "default", StringComparison.OrdinalIgnoreCase)
? null
: DashboardId;
var resolved = !string.IsNullOrWhiteSpace(requestedId)
? dashboards.FirstOrDefault(d => string.Equals(d.Id, requestedId, StringComparison.OrdinalIgnoreCase))
: dashboards.FirstOrDefault(d => string.Equals(d.Id, "DefaultDashboard", StringComparison.OrdinalIgnoreCase))
?? dashboards.FirstOrDefault();
if (resolved == null)
{
return;
}
SelectedDashboardId = resolved.Id;
if (!string.Equals(DashboardId, resolved.Id, StringComparison.OrdinalIgnoreCase))
{
Navigation.NavigateTo($"dashboards/{resolved.Id}?mode={(IsDesigner ? "designer" : "viewer")}", replace: true);
}
}
private void ToggleMode()
{
var targetMode = IsDesigner ? "viewer" : "designer";
Navigation.NavigateTo($"dashboards/{SelectedDashboardId}?mode={targetMode}", replace: true);
}
}

View File

@@ -1,7 +0,0 @@
@page "/"
<PageTitle>Home</PageTitle>
<h1>Db First approach</h1>
This is a Blazor WebAssembly application demonstrating the Database First approach using DevExpress components.

View File

@@ -1,22 +0,0 @@
/* Initialisiert die Blazor WebAssembly-Anwendung.
Registriert Root-Komponenten
Konfiguriert Abhängigkeiten */
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using DbFirst.BlazorWasm;
using DbFirst.BlazorWasm.Services;
using DevExpress.Blazor;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");
builder.Services.AddDevExpressBlazor();
var apiBaseUrl = builder.Configuration["ApiBaseUrl"] ?? builder.HostEnvironment.BaseAddress;
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(apiBaseUrl) });
builder.Services.AddScoped<CatalogApiClient>();
builder.Services.AddScoped<DashboardApiClient>();
await builder.Build().RunAsync();

View File

@@ -1,41 +0,0 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:12804",
"sslPort": 44394
}
},
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
"applicationUrl": "http://localhost:5101",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
"applicationUrl": "https://localhost:7276;http://localhost:5101",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@@ -1,148 +0,0 @@
/* Kapselt die Kommunikation mit der API für den Catalog-Endpunkt.
Bietet Methoden für CRUD-Operationen auf Catalog-Daten */
using System.Net;
using System.Net.Http.Json;
using System.Text.Json;
using DbFirst.BlazorWasm.Models;
namespace DbFirst.BlazorWasm.Services;
public class CatalogApiClient
{
private readonly HttpClient _httpClient;
private const string Endpoint = "api/catalogs";
public CatalogApiClient(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task<List<CatalogReadDto>> GetAllAsync()
{
var result = await _httpClient.GetFromJsonAsync<List<CatalogReadDto>>(Endpoint);
return result ?? new List<CatalogReadDto>();
}
public async Task<CatalogReadDto?> GetByIdAsync(int id)
{
return await _httpClient.GetFromJsonAsync<CatalogReadDto>($"{Endpoint}/{id}");
}
public async Task<ApiResult<CatalogReadDto?>> CreateAsync(CatalogWriteDto dto)
{
var response = await _httpClient.PostAsJsonAsync(Endpoint, dto);
if (response.IsSuccessStatusCode)
{
var payload = await response.Content.ReadFromJsonAsync<CatalogReadDto>();
return ApiResult<CatalogReadDto?>.Ok(payload);
}
var error = await ReadErrorAsync(response);
return ApiResult<CatalogReadDto?>.Fail(error);
}
public async Task<ApiResult<bool>> UpdateAsync(int id, CatalogWriteDto dto)
{
var response = await _httpClient.PutAsJsonAsync($"{Endpoint}/{id}", dto);
if (response.IsSuccessStatusCode)
{
return ApiResult<bool>.Ok(true);
}
var error = await ReadErrorAsync(response);
return ApiResult<bool>.Fail(error);
}
public async Task<ApiResult<bool>> DeleteAsync(int id)
{
var response = await _httpClient.DeleteAsync($"{Endpoint}/{id}");
if (response.IsSuccessStatusCode)
{
return ApiResult<bool>.Ok(true);
}
var error = await ReadErrorAsync(response);
return ApiResult<bool>.Fail(error);
}
private static async Task<string> ReadErrorAsync(HttpResponseMessage response)
{
// Liest und analysiert Fehlerdetails aus der API-Antwort.
// Gibt eine benutzerfreundliche Fehlermeldung zurück.
string? problemTitle = null;
string? problemDetail = null;
try
{
var problem = await response.Content.ReadFromJsonAsync<ProblemDetailsDto>();
if (problem != null)
{
problemTitle = problem.Title;
problemDetail = problem.Detail ?? problem.Type;
}
}
catch
{
// ignore parse errors
}
var status = response.StatusCode;
var reason = response.ReasonPhrase;
var body = await response.Content.ReadAsStringAsync();
string detail = problemDetail;
if (string.IsNullOrWhiteSpace(detail) && !string.IsNullOrWhiteSpace(body))
{
detail = body;
}
// Friendly overrides
if (status == HttpStatusCode.Conflict)
{
return "Datensatz existiert bereits. Bitte wählen Sie einen anderen Titel.";
}
if (status == HttpStatusCode.BadRequest && (detail?.Contains("CatTitle cannot be changed", StringComparison.OrdinalIgnoreCase) ?? false))
{
return "Titel kann nicht geändert werden.";
}
return status switch
{
HttpStatusCode.BadRequest => $"Eingabe ungültig{FormatSuffix(problemTitle, detail, reason)}",
HttpStatusCode.NotFound => $"Nicht gefunden{FormatSuffix(problemTitle, detail, reason)}",
HttpStatusCode.Conflict => $"Konflikt{FormatSuffix(problemTitle, detail, reason)}",
HttpStatusCode.Unauthorized => $"Nicht autorisiert{FormatSuffix(problemTitle, detail, reason)}",
HttpStatusCode.Forbidden => $"Nicht erlaubt{FormatSuffix(problemTitle, detail, reason)}",
HttpStatusCode.InternalServerError => $"Serverfehler{FormatSuffix(problemTitle, detail, reason)}",
_ => $"Fehler {(int)status} {reason ?? string.Empty}{FormatSuffix(problemTitle, detail, reason)}"
};
}
private static string FormatSuffix(string? title, string? detail, string? reason)
{
// Formatiert zusätzliche Informationen für Fehlermeldungen.
// Kombiniert Titel, Details und Grund in einer lesbaren Form.
var parts = new List<string>();
if (!string.IsNullOrWhiteSpace(title)) parts.Add(title);
if (!string.IsNullOrWhiteSpace(detail)) parts.Add(detail);
if (parts.Count == 0 && !string.IsNullOrWhiteSpace(reason)) parts.Add(reason);
if (parts.Count == 0) return string.Empty;
return ": " + string.Join(" | ", parts);
}
}
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);
}
internal sealed class ProblemDetailsDto
{
public string? Type { get; set; }
public string? Title { get; set; }
public string? Detail { get; set; }
}

View File

@@ -1,21 +0,0 @@
using System.Net.Http.Json;
using DbFirst.BlazorWasm.Models;
namespace DbFirst.BlazorWasm.Services;
public class DashboardApiClient
{
private readonly HttpClient _httpClient;
private const string Endpoint = "api/dashboard/dashboards";
public DashboardApiClient(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task<List<DashboardInfoDto>> GetAllAsync()
{
var result = await _httpClient.GetFromJsonAsync<List<DashboardInfoDto>>(Endpoint);
return result ?? new List<DashboardInfoDto>();
}
}

View File

@@ -1,16 +0,0 @@
@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.AspNetCore.Components.WebAssembly.Http
@using Microsoft.JSInterop
@using DbFirst.BlazorWasm
@using DbFirst.BlazorWasm.Layout
@using DbFirst.BlazorWasm.Models
@using DbFirst.BlazorWasm.Services
@using DbFirst.BlazorWasm.Components
@using DevExpress.Blazor
@using DevExpress.DashboardBlazor
@using DevExpress.DashboardWeb

View File

@@ -1,3 +0,0 @@
{
"ApiBaseUrl": "https://localhost:7204/"
}

View File

@@ -1,103 +0,0 @@
html, body {
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
}
h1:focus {
outline: none;
}
a, .btn-link {
color: #0071c1;
}
.btn-primary {
color: #fff;
background-color: #1b6ec2;
border-color: #1861ac;
}
.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
}
.content {
padding-top: 1.1rem;
}
.valid.modified:not([type=checkbox]) {
outline: 1px solid #26b050;
}
.invalid {
outline: 1px solid red;
}
.validation-message {
color: red;
}
#blazor-error-ui {
background: lightyellow;
bottom: 0;
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
display: none;
left: 0;
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
position: fixed;
width: 100%;
z-index: 1000;
}
#blazor-error-ui .dismiss {
cursor: pointer;
position: absolute;
right: 0.75rem;
top: 0.5rem;
}
.blazor-error-boundary {
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
padding: 1rem 1rem 1rem 3.7rem;
color: white;
}
.blazor-error-boundary::after {
content: "An error has occurred."
}
.loading-progress {
position: relative;
display: block;
width: 8rem;
height: 8rem;
margin: 20vh auto 1rem auto;
}
.loading-progress circle {
fill: none;
stroke: #e0e0e0;
stroke-width: 0.6rem;
transform-origin: 50% 50%;
transform: rotate(-90deg);
}
.loading-progress circle:last-child {
stroke: #1b6ec2;
stroke-dasharray: calc(3.141 * var(--blazor-load-percentage, 0%) * 0.8), 500%;
transition: stroke-dasharray 0.05s ease-in-out;
}
.loading-progress-text {
position: absolute;
text-align: center;
font-weight: bold;
inset: calc(20vh + 3.25rem) 0 auto 0.2rem;
}
.loading-progress-text:after {
content: var(--blazor-load-percentage-text, "Loading");
}
code {
color: #c02d76;
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -1,58 +0,0 @@
<!--
• Ist der technische Einstiegspunkt der Blazor WebAssembly-Anwendung.
• Sie lädt die notwendigen Ressourcen (z. B. das Blazor-Skript blazor.webassembly.js)
und definiert den Platzhalter <div id="app">, in dem die Blazor-Komponenten gerendert werden.
• Ohne diese Datei könnte die Blazor-Anwendung nicht starten, da sie die Verbindung
zwischen der statischen HTML-Welt und der Blazor-Welt herstellt.
kurz: Startet die Anwendung und lädt die Blazor-Umgebung.
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>DbFirst.BlazorWasm</title>
<base href="/" />
<!-- Stylesheets für DevExpress und Bootstrap -->
<link href="_content/DevExpress.Blazor.Dashboard/ace.css" rel="stylesheet" />
<link href="_content/DevExpress.Blazor.Dashboard/ace-theme-dreamweaver.css" rel="stylesheet" />
<link href="_content/DevExpress.Blazor.Dashboard/ace-theme-ambiance.css" rel="stylesheet" />
<link href="_content/DevExpress.Blazor.Dashboard/dx.light.css" rel="stylesheet" />
<link href="_content/DevExpress.Blazor.Dashboard/dx-analytics.common.css" rel="stylesheet" />
<link href="_content/DevExpress.Blazor.Dashboard/dx-analytics.light.css" rel="stylesheet" />
<link href="_content/DevExpress.Blazor.Dashboard/dx-querybuilder.css" rel="stylesheet" />
<link href="_content/DevExpress.Blazor.Dashboard/dx-dashboard.light.min.css" rel="stylesheet" />
<link rel="stylesheet" href="css/bootstrap/bootstrap.min.css" />
<link rel="stylesheet" href="css/app.css" />
<link rel="icon" type="image/png" href="favicon.png" />
<link href="DbFirst.BlazorWasm.styles.css" rel="stylesheet" />
</head>
<body>
<!-- Einstiegspunkt der Blazor-Anwendung -->
<div id="app">
<svg class="loading-progress">
<circle r="40%" cx="50%" cy="50%" />
<circle r="40%" cx="50%" cy="50%" />
</svg>
<div class="loading-progress-text"></div>
</div>
<!-- Fehler-UI für unvorhergesehene Fehler -->
<div id="blazor-error-ui">
An unhandled error has occurred.
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>
<!-- Blazor WebAssembly-Skript -->
<script src="_framework/blazor.webassembly.js"></script>
</body>
</html>

View File

@@ -5,7 +5,10 @@
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<base href="/" /> <base href="/" />
@DxResourceManager.RegisterTheme(Themes.Fluent) @DxResourceManager.RegisterTheme(Themes.Fluent.Clone(properties =>
{
properties.ApplyToPageElements = true;
}))
@DxResourceManager.RegisterScripts() @DxResourceManager.RegisterScripts()
<link href="_content/DevExpress.Blazor.Dashboard/ace.css" rel="stylesheet" /> <link href="_content/DevExpress.Blazor.Dashboard/ace.css" rel="stylesheet" />
@@ -17,17 +20,12 @@
<link href="_content/DevExpress.Blazor.Dashboard/dx-querybuilder.css" rel="stylesheet" /> <link href="_content/DevExpress.Blazor.Dashboard/dx-querybuilder.css" rel="stylesheet" />
<link href="_content/DevExpress.Blazor.Dashboard/dx-dashboard.light.min.css" rel="stylesheet" /> <link href="_content/DevExpress.Blazor.Dashboard/dx-dashboard.light.min.css" rel="stylesheet" />
<link rel="stylesheet" href="_content/DevExpress.Blazor.Themes/bootstrap-external.bs5.min.css" />
<link rel="stylesheet" href="_content/DevExpress.Blazor.Themes.Fluent/core.min.css" />
<link rel="stylesheet" href="_content/DevExpress.Blazor.Themes.Fluent/global.min.css" />
<link rel="stylesheet" href="_content/DevExpress.Blazor.Themes.Fluent/modes/light.min.css" />
<link rel="stylesheet" href="_content/DevExpress.Blazor.Themes.Fluent/accents/blue.min.css" />
<link rel="stylesheet" href="_content/DevExpress.Blazor.Themes.Fluent/bootstrap/fluent-light.bs5.min.css" />
<link rel="stylesheet" href="bootstrap/bootstrap.min.css" /> <link rel="stylesheet" href="bootstrap/bootstrap.min.css" />
<link rel="stylesheet" href="app.css" /> <link rel="stylesheet" href="app.css" />
<link rel="stylesheet" href="DbFirst.BlazorWebApp.styles.css" /> <link rel="stylesheet" href="DbFirst.BlazorWebApp.styles.css" />
<link rel="icon" type="image/png" href="favicon.png" /> <link rel="icon" type="image/png" href="favicon.png" />
<script src="js/size-manager.js"></script>
<HeadOutlet /> <HeadOutlet />
</head> </head>

View File

@@ -1,8 +1,59 @@
@using System.Text.Json
@using Microsoft.AspNetCore.Components
@using Microsoft.AspNetCore.Components.Rendering
@using Microsoft.AspNetCore.Components.Forms
@using DevExpress.Blazor
@using DevExpress.Data.Filtering
@inject CatalogApiClient Api @inject CatalogApiClient Api
@inject LayoutApiClient LayoutApi
@inject IJSRuntime JsRuntime
<style> <style>
.action-panel { margin-bottom: 16px; } .action-panel { margin-bottom: 16px; }
.grid-section { margin-top: 12px; } .grid-section { margin-top: 12px; }
.catalog-edit-popup {
min-width: 720px;
}
.band-editor {
display: grid;
gap: 12px;
margin-bottom: 16px;
}
.band-controls {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
}
.band-row {
display: flex;
gap: 8px;
align-items: center;
}
.band-columns {
max-width: 720px;
}
.filter-row-cell {
display: flex;
align-items: center;
gap: 4px;
flex-wrap: wrap;
}
.filter-operator {
width: 52px;
min-width: 52px;
flex: 0 0 52px;
}
.filter-value {
min-width: 160px;
flex: 1 1 160px;
}
.loading-container {
min-height: 160px;
display: flex;
align-items: center;
justify-content: center;
}
</style> </style>
@if (!string.IsNullOrWhiteSpace(errorMessage)) @if (!string.IsNullOrWhiteSpace(errorMessage))
@@ -14,84 +65,155 @@ else if (!string.IsNullOrWhiteSpace(infoMessage))
<div class="alert alert-success" role="alert">@infoMessage</div> <div class="alert alert-success" role="alert">@infoMessage</div>
} }
<div class="mb-3"> @if (!hasLoaded || isLoading)
<DxButton RenderStyle="ButtonRenderStyle.Primary" Click="@StartCreate">Neuen Eintrag anlegen</DxButton>
</div>
@if (showForm)
{ {
<div class="action-panel"> <div class="loading-container">
<EditForm Model="formModel" OnValidSubmit="HandleSubmit" Context="editCtx"> <div class="spinner-border text-primary" role="status">
<DxFormLayout ColCount="2"> <span class="visually-hidden">Lade...</span>
<DxFormLayoutItem Caption="Titel" Context="itemCtx"> </div>
<DxTextBox @bind-Text="formModel.CatTitle" Enabled="@(isEditing ? formModel.UpdateProcedure != 0 : true)" />
</DxFormLayoutItem>
<DxFormLayoutItem Caption="Kennung" Context="itemCtx">
<DxTextBox @bind-Text="formModel.CatString" />
</DxFormLayoutItem>
@if (isEditing)
{
<DxFormLayoutItem Caption="Update-Prozedur" Context="itemCtx">
<DxComboBox Data="@procedureOptions"
TextFieldName="Text"
ValueFieldName="Value"
@bind-Value="formModel.UpdateProcedure" />
</DxFormLayoutItem>
}
<DxFormLayoutItem Caption=" " Context="itemCtx">
<DxStack Orientation="Orientation.Horizontal" Spacing="8">
<DxButton RenderStyle="ButtonRenderStyle.Success" ButtonType="ButtonType.Submit" SubmitFormOnClick="true" Context="btnCtx">@((isEditing ? "Speichern" : "Anlegen"))</DxButton>
<DxButton RenderStyle="ButtonRenderStyle.Secondary" Click="@CancelEdit" Context="btnCtx">Abbrechen</DxButton>
</DxStack>
</DxFormLayoutItem>
</DxFormLayout>
</EditForm>
</div> </div>
} }
@if (isLoading)
{
<p><em>Lade Daten...</em></p>
}
else if (items.Count == 0) else if (items.Count == 0)
{ {
<p>Keine Einträge vorhanden.</p> <p>Keine Eintr&#228;ge vorhanden.</p>
} }
else else
{ {
<div class="band-editor">
<div class="band-controls">
<DxButton Text="Band hinzuf&#252;gen" Click="AddBand" />
<DxButton Text="Layout speichern" Click="SaveLayoutAsync" Enabled="@CanSaveBandLayout" />
<DxButton Text="Band-Layout zur&#252;cksetzen" Click="ResetBandLayoutAsync" />
</div>
@foreach (var band in bandLayout.Bands)
{
<div class="band-row">
<DxTextBox Text="@band.Caption" TextChanged="@(value => UpdateBandCaption(band, value))" />
<DxButton Text="Entfernen" Click="@(() => RemoveBand(band))" />
</div>
}
<DxFormLayout CssClass="band-columns" ColCount="2">
@foreach (var column in columnDefinitions)
{
<DxFormLayoutItem Caption="@column.Caption">
<DxComboBox Data="@bandOptions"
TData="BandOption"
TValue="string"
TextFieldName="Caption"
ValueFieldName="Id"
Value="@GetColumnBand(column.FieldName)"
ValueChanged="@(value => UpdateColumnBand(column.FieldName, value))"
Width="100%" />
</DxFormLayoutItem>
}
</DxFormLayout>
</div>
<div class="grid-section"> <div class="grid-section">
<DxGrid Data="@items" TItem="CatalogReadDto" KeyFieldName="@nameof(CatalogReadDto.Guid)" ShowFilterRow="true" PageSize="10" CssClass="mb-4 catalog-grid"> <DxGrid Data="@items"
TItem="CatalogReadDto"
KeyFieldName="@nameof(CatalogReadDto.Guid)"
SizeMode="@_sizeMode"
ShowGroupPanel="true"
ShowGroupedColumns="true"
AllowGroup="true"
FilterMenuButtonDisplayMode="GridFilterMenuButtonDisplayMode.Always"
AllowColumnResize="true"
ColumnResizeMode="GridColumnResizeMode.ColumnsContainer"
AllowColumnReorder="true"
PageSize="10"
CssClass="mb-4 catalog-grid"
EditMode="GridEditMode.PopupEditForm"
PopupEditFormCssClass="catalog-edit-popup"
PopupEditFormHeaderText="@popupHeaderText"
CustomizeEditModel="OnCustomizeEditModel"
EditModelSaving="OnEditModelSaving"
DataItemDeleting="OnDataItemDeleting"
FocusedRowEnabled="true"
@bind-FocusedRowKey="focusedRowKey"
@ref="gridRef">
<ToolbarTemplate>
<DxToolbar>
<DxToolbarItem Alignment="ToolbarItemAlignment.Right">
<Template Context="_">
<DxDropDownButton Text="@FormatSizeText(_sizeMode)"
RenderStyle="ButtonRenderStyle.Secondary"
RenderStyleMode="ButtonRenderStyleMode.Text"
ItemClick="OnSizeChange">
<Items>
@foreach (var size in _sizeModes)
{
<DxDropDownButtonItem Text="@FormatSizeText(size)" Id="@size.ToString()" />
}
</Items>
</DxDropDownButton>
</Template>
</DxToolbarItem>
</DxToolbar>
</ToolbarTemplate>
<Columns> <Columns>
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.Guid)" Caption="Id" Width="140px" SortIndex="0" SortOrder="GridColumnSortOrder.Ascending" /> @RenderColumns()
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.CatTitle)" Caption="Titel" />
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.CatString)" Caption="String" />
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.AddedWho)" Caption="Angelegt von" />
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.AddedWhen)" Caption="Angelegt am" />
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.ChangedWho)" Caption="Geändert von" />
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.ChangedWhen)" Caption="Geändert am" />
<DxGridDataColumn Caption="" Width="220px" AllowSort="false">
<CellDisplayTemplate Context="cell">
@{ var item = (CatalogReadDto)cell.DataItem; }
<div style="white-space: nowrap;">
<DxButton RenderStyle="ButtonRenderStyle.Secondary" Size="ButtonSize.Small" Click="@(() => StartEdit(item))">Bearbeiten</DxButton>
<DxButton RenderStyle="ButtonRenderStyle.Danger" Size="ButtonSize.Small" Click="@(() => DeleteCatalog(item.Guid))">Löschen</DxButton>
</div>
</CellDisplayTemplate>
</DxGridDataColumn>
</Columns> </Columns>
<EditFormTemplate Context="editFormContext">
@{ SetEditContext(editFormContext.EditContext); var editModel = (CatalogEditModel)editFormContext.EditModel; SetPopupHeaderText(editModel.IsNew); }
<DxFormLayout ColCount="2">
<DxFormLayoutItem Caption="Titel">
<DxTextBox @bind-Text="editModel.CatTitle" Width="100%" />
</DxFormLayoutItem>
<DxFormLayoutItem Caption="Kennung">
<DxTextBox @bind-Text="editModel.CatString" Width="100%" />
</DxFormLayoutItem>
@if (!editModel.IsNew)
{
<DxFormLayoutItem Caption="Update-Prozedur">
<DxComboBox Data="@procedureOptions"
TData="ProcedureOption"
TValue="int"
TextFieldName="Text"
ValueFieldName="Value"
@bind-Value="editModel.UpdateProcedure"
Width="100%" />
</DxFormLayoutItem>
}
<DxFormLayoutItem ColSpanMd="12">
<ValidationSummary />
</DxFormLayoutItem>
</DxFormLayout>
</EditFormTemplate>
</DxGrid> </DxGrid>
</div> </div>
} }
@code { @code {
private List<CatalogReadDto> items = new(); private List<CatalogReadDto> items = new();
private CatalogWriteDto formModel = new();
private int editingId;
private bool isLoading; private bool isLoading;
private bool isEditing; private bool hasLoaded;
private bool showForm;
private string? errorMessage; private string? errorMessage;
private string? infoMessage; private string? infoMessage;
private EditContext? editContext;
private ValidationMessageStore? validationMessageStore;
private IGrid? gridRef;
private int? focusedRowKey;
private string popupHeaderText = "Edit";
private const string LayoutType = "GRID_BANDS";
private const string LayoutKey = "CatalogsGrid";
private const string LayoutUserStorageKey = "layoutUser";
private string? layoutUser;
private BandLayout bandLayout = new();
private Dictionary<string, string> columnBandAssignments = new();
private List<BandOption> bandOptions = new();
private Dictionary<string, ColumnDefinition> columnLookup = new();
private readonly JsonSerializerOptions jsonOptions = new(JsonSerializerDefaults.Web);
private List<ColumnDefinition> columnDefinitions = new()
{
new() { FieldName = nameof(CatalogReadDto.Guid), Caption = "Id", Width = "140px", FilterType = ColumnFilterType.Text },
new() { FieldName = nameof(CatalogReadDto.CatTitle), Caption = "Titel", FilterType = ColumnFilterType.Text },
new() { FieldName = nameof(CatalogReadDto.CatString), Caption = "String", FilterType = ColumnFilterType.Text },
new() { FieldName = nameof(CatalogReadDto.AddedWho), Caption = "Angelegt von", ReadOnly = true, FilterType = ColumnFilterType.Text },
new() { FieldName = nameof(CatalogReadDto.AddedWhen), Caption = "Angelegt am", ReadOnly = true, FilterType = ColumnFilterType.Date },
new() { FieldName = nameof(CatalogReadDto.ChangedWho), Caption = "Ge&#228;ndert von", ReadOnly = true, FilterType = ColumnFilterType.Text },
new() { FieldName = nameof(CatalogReadDto.ChangedWhen), Caption = "Ge&#228;ndert am", ReadOnly = true, FilterType = ColumnFilterType.Date }
};
private readonly List<ProcedureOption> procedureOptions = new() private readonly List<ProcedureOption> procedureOptions = new()
{ {
@@ -99,11 +221,109 @@ else
new() { Value = 1, Text = "PRTBMY_CATALOG_SAVE" } new() { Value = 1, Text = "PRTBMY_CATALOG_SAVE" }
}; };
private bool CanSaveBandLayout => !string.IsNullOrWhiteSpace(layoutUser);
private bool gridLayoutApplied;
private DevExpress.Blazor.SizeMode _sizeMode = DevExpress.Blazor.SizeMode.Medium;
private static readonly List<DevExpress.Blazor.SizeMode> _sizeModes =
Enum.GetValues<DevExpress.Blazor.SizeMode>().ToList();
private string FormatSizeText(DevExpress.Blazor.SizeMode size) => size switch
{
DevExpress.Blazor.SizeMode.Small => "Klein",
DevExpress.Blazor.SizeMode.Medium => "Mittel",
DevExpress.Blazor.SizeMode.Large => "Groß",
_ => size.ToString()
};
private void OnSizeChange(DropDownButtonItemClickEventArgs args)
{
_sizeMode = Enum.Parse<DevExpress.Blazor.SizeMode>(args.ItemInfo.Id);
}
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
columnLookup = columnDefinitions.ToDictionary(column => column.FieldName, StringComparer.OrdinalIgnoreCase);
await EnsureLayoutUserAsync();
await LoadBandLayoutAsync();
await LoadCatalogs(); await LoadCatalogs();
} }
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (!gridLayoutApplied && gridRef != null && bandLayout.GridLayout != null)
{
gridRef.LoadLayout(bandLayout.GridLayout);
gridLayoutApplied = true;
await InvokeAsync(StateHasChanged);
}
}
private void SetEditContext(EditContext context)
{
if (editContext == context)
{
return;
}
if (editContext != null)
{
editContext.OnFieldChanged -= OnEditFieldChanged;
}
editContext = context;
validationMessageStore = new ValidationMessageStore(editContext);
editContext.OnFieldChanged += OnEditFieldChanged;
}
private void OnEditFieldChanged(object? sender, FieldChangedEventArgs e)
{
if (validationMessageStore == null || editContext == null)
{
return;
}
if (e.FieldIdentifier.FieldName == nameof(CatalogEditModel.UpdateProcedure))
{
validationMessageStore.Clear();
editContext.NotifyValidationStateChanged();
return;
}
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)
{
popupHeaderText = isNew ? "Neu" : "Edit";
}
private void OnCustomizeEditModel(GridCustomizeEditModelEventArgs e)
{
popupHeaderText = e.IsNew ? "Neu" : "Edit";
if (e.IsNew)
{
e.EditModel = new CatalogEditModel { IsNew = true };
return;
}
var item = (CatalogReadDto)e.DataItem;
e.EditModel = new CatalogEditModel
{
Guid = item.Guid,
CatTitle = item.CatTitle,
CatString = item.CatString,
UpdateProcedure = 0,
OriginalCatTitle = item.CatTitle,
IsNew = false
};
}
private async Task LoadCatalogs() private async Task LoadCatalogs()
{ {
isLoading = true; isLoading = true;
@@ -119,107 +339,471 @@ else
finally finally
{ {
isLoading = false; isLoading = false;
hasLoaded = true;
StateHasChanged(); StateHasChanged();
} }
} }
private void StartCreate() private async Task OnEditModelSaving(GridEditModelSavingEventArgs e)
{ {
formModel = new CatalogWriteDto();
editingId = 0;
isEditing = false;
showForm = true;
infoMessage = null;
errorMessage = null; errorMessage = null;
} infoMessage = null;
private void StartEdit(CatalogReadDto item) validationMessageStore?.Clear();
{ editContext?.NotifyValidationStateChanged();
formModel = new CatalogWriteDto
var editModel = (CatalogEditModel)e.EditModel;
if (!ValidateEditModel(editModel, e.IsNew))
{ {
CatTitle = item.CatTitle, e.Cancel = true;
CatString = item.CatString, return;
UpdateProcedure = 0 }
};
editingId = item.Guid;
isEditing = true;
showForm = true;
infoMessage = null;
errorMessage = null;
}
private async Task HandleSubmit() var dto = new CatalogWriteDto
{ {
errorMessage = null; CatTitle = editModel.CatTitle,
infoMessage = null; CatString = editModel.CatString,
UpdateProcedure = editModel.UpdateProcedure
};
try try
{ {
if (isEditing) if (e.IsNew)
{ {
var updated = await Api.UpdateAsync(editingId, formModel); var created = await Api.CreateAsync(dto);
if (!updated.Success)
{
errorMessage = updated.Error ?? "Aktualisierung fehlgeschlagen.";
return;
}
infoMessage = "Katalog aktualisiert.";
}
else
{
var created = await Api.CreateAsync(formModel);
if (!created.Success || created.Value == null) if (!created.Success || created.Value == null)
{ {
errorMessage = created.Error ?? "Anlegen fehlgeschlagen."; if (!string.IsNullOrWhiteSpace(created.Error))
{
AddValidationError(editModel, nameof(CatalogEditModel.CatTitle), created.Error);
}
else
{
errorMessage = "Anlegen fehlgeschlagen.";
}
e.Cancel = true;
return; return;
} }
infoMessage = "Katalog angelegt."; infoMessage = "Katalog angelegt.";
focusedRowKey = created.Value.Guid;
}
else
{
var updated = await Api.UpdateAsync(editModel.Guid, dto);
if (!updated.Success)
{
errorMessage = updated.Error ?? "Aktualisierung fehlgeschlagen.";
e.Cancel = true;
return;
}
infoMessage = "Katalog aktualisiert.";
focusedRowKey = editModel.Guid;
} }
showForm = false;
await LoadCatalogs(); await LoadCatalogs();
} }
catch (Exception ex) catch (Exception ex)
{ {
errorMessage = $"Fehler beim Speichern: {ex.Message}"; errorMessage = $"Fehler beim Speichern: {ex.Message}";
e.Cancel = true;
} }
} }
private void CancelEdit() private void AddValidationError(CatalogEditModel editModel, string fieldName, string message)
{ {
showForm = false; if (editContext == null || validationMessageStore == null)
infoMessage = null; {
errorMessage = null; return;
}
var field = new FieldIdentifier(editModel, fieldName);
validationMessageStore.Add(field, message);
editContext.NotifyValidationStateChanged();
} }
private async Task DeleteCatalog(int id) private bool ValidateEditModel(CatalogEditModel editModel, bool isNew)
{
if (isNew)
{
return true;
}
if (editModel.UpdateProcedure == 0 &&
!string.Equals(editModel.CatTitle, editModel.OriginalCatTitle, StringComparison.OrdinalIgnoreCase))
{
AddValidationError(editModel, nameof(CatalogEditModel.CatTitle), "Titel kann nicht ge&#228;ndert werden.");
return false;
}
return true;
}
private async Task OnDataItemDeleting(GridDataItemDeletingEventArgs e)
{ {
errorMessage = null; errorMessage = null;
infoMessage = null; infoMessage = null;
var item = (CatalogReadDto)e.DataItem;
try try
{ {
var deleted = await Api.DeleteAsync(id); var deleted = await Api.DeleteAsync(item.Guid);
if (!deleted.Success) if (!deleted.Success)
{ {
errorMessage = deleted.Error ?? "Löschen fehlgeschlagen."; errorMessage = deleted.Error ?? "L&#246;schen fehlgeschlagen.";
e.Cancel = true;
return; return;
} }
infoMessage = "Katalog gelöscht."; infoMessage = "Katalog gel&#246;scht.";
await LoadCatalogs(); await LoadCatalogs();
} }
catch (Exception ex) catch (Exception ex)
{ {
errorMessage = $"Fehler beim Löschen: {ex.Message}"; errorMessage = $"Fehler beim L&#246;schen: {ex.Message}";
e.Cancel = true;
} }
} }
private async Task EnsureLayoutUserAsync()
{
layoutUser = await JsRuntime.InvokeAsync<string?>("localStorage.getItem", LayoutUserStorageKey);
if (string.IsNullOrWhiteSpace(layoutUser))
{
layoutUser = Guid.NewGuid().ToString("N");
await JsRuntime.InvokeVoidAsync("localStorage.setItem", LayoutUserStorageKey, layoutUser);
}
}
private async Task SaveLayoutAsync()
{
if (string.IsNullOrWhiteSpace(layoutUser))
{
return;
}
try
{
CaptureColumnLayoutFromGrid();
var layoutData = JsonSerializer.Serialize(bandLayout, jsonOptions);
await LayoutApi.UpsertAsync(new LayoutDto
{
LayoutType = LayoutType,
LayoutKey = LayoutKey,
UserName = layoutUser,
LayoutData = layoutData
});
infoMessage = "Layout gespeichert.";
errorMessage = null;
}
catch (Exception ex)
{
errorMessage = $"Layout konnte nicht gespeichert werden: {ex.Message}";
}
}
private void CaptureColumnLayoutFromGrid()
{
if (gridRef == null)
{
return;
}
var layout = gridRef.SaveLayout();
bandLayout.GridLayout = layout;
var orderedColumns = layout.Columns
.Where(column => !string.IsNullOrWhiteSpace(column.FieldName))
.OrderBy(column => column.VisibleIndex)
.ToList();
bandLayout.ColumnOrder = orderedColumns
.Select(column => column.FieldName)
.ToList();
bandLayout.ColumnWidths = orderedColumns
.Where(column => !string.IsNullOrWhiteSpace(column.Width))
.ToDictionary(column => column.FieldName, column => column.Width, StringComparer.OrdinalIgnoreCase);
}
private async Task LoadBandLayoutAsync()
{
if (string.IsNullOrWhiteSpace(layoutUser))
{
bandLayout = new BandLayout();
UpdateBandOptions();
return;
}
var stored = await LayoutApi.GetAsync(LayoutType, LayoutKey, layoutUser);
if (stored != null && !string.IsNullOrWhiteSpace(stored.LayoutData))
{
var parsed = JsonSerializer.Deserialize<BandLayout>(stored.LayoutData, jsonOptions);
bandLayout = NormalizeBandLayout(parsed);
}
else
{
bandLayout = new BandLayout();
}
columnBandAssignments = BuildAssignmentsFromLayout(bandLayout);
ApplyColumnLayoutFromStorage();
UpdateBandOptions();
}
private async Task ResetBandLayoutAsync()
{
if (string.IsNullOrWhiteSpace(layoutUser))
{
return;
}
await LayoutApi.DeleteAsync(LayoutType, LayoutKey, layoutUser);
bandLayout = new BandLayout();
columnBandAssignments.Clear();
UpdateBandOptions();
infoMessage = "Band-Layout zur&#252;ckgesetzt.";
}
private void ApplyColumnLayoutFromStorage()
{
foreach (var column in columnDefinitions)
{
if (bandLayout.ColumnWidths.TryGetValue(column.FieldName, out var width) && !string.IsNullOrWhiteSpace(width))
{
column.Width = width;
}
}
columnLookup = columnDefinitions.ToDictionary(column => column.FieldName, StringComparer.OrdinalIgnoreCase);
}
private void AddBand()
{
bandLayout.Bands.Add(new BandDefinition
{
Id = Guid.NewGuid().ToString("N"),
Caption = "Band"
});
UpdateBandOptions();
}
private void RemoveBand(BandDefinition band)
{
bandLayout.Bands.Remove(band);
var removedColumns = columnBandAssignments.Where(pair => pair.Value == band.Id)
.Select(pair => pair.Key)
.ToList();
foreach (var column in removedColumns)
{
columnBandAssignments.Remove(column);
}
UpdateBandOptions();
SyncBandsFromAssignments();
}
private void UpdateBandCaption(BandDefinition band, string value)
{
band.Caption = value;
UpdateBandOptions();
}
private void UpdateColumnBand(string fieldName, string? bandId)
{
if (string.IsNullOrWhiteSpace(bandId))
{
columnBandAssignments.Remove(fieldName);
}
else
{
columnBandAssignments[fieldName] = bandId;
}
SyncBandsFromAssignments();
}
private string GetColumnBand(string fieldName)
{
return columnBandAssignments.TryGetValue(fieldName, out var bandId) ? bandId : string.Empty;
}
private void SyncBandsFromAssignments()
{
foreach (var band in bandLayout.Bands)
{
band.Columns = columnDefinitions
.Where(column => columnBandAssignments.TryGetValue(column.FieldName, out var bandId) && bandId == band.Id)
.Select(column => column.FieldName)
.ToList();
}
StateHasChanged();
}
private void UpdateBandOptions()
{
bandOptions = new List<BandOption> { new() { Id = string.Empty, Caption = "Ohne Band" } };
bandOptions.AddRange(bandLayout.Bands.Select(band => new BandOption { Id = band.Id, Caption = band.Caption }));
}
private BandLayout NormalizeBandLayout(BandLayout? layout)
{
layout ??= new BandLayout();
layout.Bands ??= new List<BandDefinition>();
layout.ColumnOrder ??= new List<string>();
layout.ColumnWidths ??= new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
foreach (var band in layout.Bands)
{
if (string.IsNullOrWhiteSpace(band.Id))
{
band.Id = Guid.NewGuid().ToString("N");
}
if (string.IsNullOrWhiteSpace(band.Caption))
{
band.Caption = "Band";
}
band.Columns = band.Columns?.Where(columnLookup.ContainsKey).ToList() ?? new List<string>();
}
return layout;
}
private Dictionary<string, string> BuildAssignmentsFromLayout(BandLayout layout)
{
var assignments = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
foreach (var band in layout.Bands)
{
foreach (var column in band.Columns)
{
assignments[column] = band.Id;
}
}
return assignments;
}
private RenderFragment RenderColumns() => builder =>
{
var seq = 0;
builder.OpenComponent<DxGridCommandColumn>(seq++);
builder.AddAttribute(seq++, "Width", "120px");
builder.CloseComponent();
var grouped = bandLayout.Bands.SelectMany(band => band.Columns).ToHashSet(StringComparer.OrdinalIgnoreCase);
foreach (var column in columnDefinitions.Where(column => !grouped.Contains(column.FieldName)))
{
BuildDataColumn(builder, ref seq, column);
}
foreach (var band in bandLayout.Bands)
{
if (band.Columns.Count == 0)
{
continue;
}
builder.OpenComponent<DxGridBandColumn>(seq++);
builder.AddAttribute(seq++, "Caption", band.Caption);
builder.AddAttribute(seq++, "Columns", (RenderFragment)(bandBuilder =>
{
var bandSeq = 0;
foreach (var columnName in band.Columns)
{
if (columnLookup.TryGetValue(columnName, out var column))
{
BuildDataColumn(bandBuilder, ref bandSeq, column);
}
}
}));
builder.CloseComponent();
}
};
private void BuildDataColumn(RenderTreeBuilder builder, ref int seq, ColumnDefinition column)
{
builder.OpenComponent<DxGridDataColumn>(seq++);
builder.AddAttribute(seq++, "FieldName", column.FieldName);
builder.AddAttribute(seq++, "Caption", column.Caption);
if (!string.IsNullOrWhiteSpace(column.Width))
{
builder.AddAttribute(seq++, "Width", column.Width);
}
if (!string.IsNullOrWhiteSpace(column.DisplayFormat))
{
builder.AddAttribute(seq++, "DisplayFormat", column.DisplayFormat);
}
if (column.ReadOnly)
{
builder.AddAttribute(seq++, "ReadOnly", true);
}
builder.CloseComponent();
}
private sealed class BandLayout
{
public List<BandDefinition> Bands { get; set; } = new();
public List<string> ColumnOrder { get; set; } = new();
public Dictionary<string, string?> ColumnWidths { get; set; } = new(StringComparer.OrdinalIgnoreCase);
public GridPersistentLayout? GridLayout { get; set; }
}
private sealed class BandDefinition
{
public string Id { get; set; } = string.Empty;
public string Caption { get; set; } = string.Empty;
public List<string> Columns { get; set; } = new();
}
private sealed class BandOption
{
public string Id { get; set; } = string.Empty;
public string Caption { get; set; } = string.Empty;
}
private sealed class ColumnDefinition
{
public string FieldName { get; init; } = string.Empty;
public string Caption { get; init; } = string.Empty;
public string? Width { get; set; }
public string? DisplayFormat { get; init; }
public bool ReadOnly { get; init; }
public ColumnFilterType FilterType { get; init; }
}
private enum ColumnFilterType
{
Text,
Date
}
private sealed class CatalogEditModel
{
public int Guid { get; set; }
public string CatTitle { get; set; } = string.Empty;
public string CatString { get; set; } = string.Empty;
public int UpdateProcedure { get; set; }
public string OriginalCatTitle { get; set; } = string.Empty;
public bool IsNew { get; set; }
}
private sealed class ProcedureOption private sealed class ProcedureOption
{ {
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 sealed class FilterOperatorOption
{
public string Value { get; set; } = string.Empty;
public string Text { get; set; } = string.Empty;
}
} }

View File

@@ -1,12 +1,15 @@
@inherits LayoutComponentBase @inherits LayoutComponentBase
@implements IDisposable
@inject ThemeState ThemeState
<div class="page"> <div class="page @(ThemeState.IsDarkMode ? "app-dark" : "app-light")">
<div class="sidebar"> <div class="sidebar">
<NavMenu /> <NavMenu />
</div> </div>
<main> <main>
<div class="top-row px-4"> <div class="top-row px-4">
<DxButton Text="@(ThemeState.IsDarkMode ? "Dark Mode aus" : "Dark Mode an")" Click="ToggleTheme" />
<a href="https://learn.microsoft.com/aspnet/core/" target="_blank">About</a> <a href="https://learn.microsoft.com/aspnet/core/" target="_blank">About</a>
</div> </div>
@@ -21,3 +24,20 @@
<a href="" class="reload">Reload</a> <a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a> <a class="dismiss">🗙</a>
</div> </div>
@code {
protected override void OnInitialized()
{
ThemeState.OnChange += StateHasChanged;
}
private void ToggleTheme()
{
ThemeState.SetDarkMode(!ThemeState.IsDarkMode);
}
public void Dispose()
{
ThemeState.OnChange -= StateHasChanged;
}
}

View File

@@ -4,14 +4,27 @@
flex-direction: column; flex-direction: column;
} }
.page.app-dark {
background-color: #1b1b1b;
color: #f1f1f1;
}
main { main {
flex: 1; flex: 1;
} }
.page.app-dark main {
background-color: #1b1b1b;
}
.sidebar { .sidebar {
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%); background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
} }
.page.app-dark .sidebar {
background-image: linear-gradient(180deg, #171717 0%, #0f2a46 70%);
}
.top-row { .top-row {
background-color: #f7f7f7; background-color: #f7f7f7;
border-bottom: 1px solid #d6d5d5; border-bottom: 1px solid #d6d5d5;
@@ -21,6 +34,11 @@ main {
align-items: center; align-items: center;
} }
.page.app-dark .top-row {
background-color: #222;
border-bottom-color: #3d3d3d;
}
.top-row ::deep a, .top-row ::deep .btn-link { .top-row ::deep a, .top-row ::deep .btn-link {
white-space: nowrap; white-space: nowrap;
margin-left: 1.5rem; margin-left: 1.5rem;

View File

@@ -14,17 +14,6 @@
</NavLink> </NavLink>
</div> </div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="counter">
<span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span> Counter
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="weather">
<span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Weather
</NavLink>
</div>
<div class="nav-item px-3"> <div class="nav-item px-3">
<NavLink class="nav-link" href="catalogs"> <NavLink class="nav-link" href="catalogs">
<span class="bi bi-collection-nav-menu" aria-hidden="true"></span> Catalogs <span class="bi bi-collection-nav-menu" aria-hidden="true"></span> Catalogs
@@ -33,7 +22,13 @@
<div class="nav-item px-3"> <div class="nav-item px-3">
<NavLink class="nav-link" href="dashboards"> <NavLink class="nav-link" href="dashboards">
<span class="oi oi-list-rich" aria-hidden="true"></span> Dashboards <span class="bi bi-speedometer-nav-menu" aria-hidden="true"></span> Dashboards
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="massdata">
<span class="bi bi-table-nav-menu" aria-hidden="true"></span> MassData
</NavLink> </NavLink>
</div> </div>
</nav> </nav>

View File

@@ -46,6 +46,18 @@
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E"); background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E");
} }
.bi-collection-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' viewBox='0 0 16 16'%3E%3Cpath d='M2 3a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 .5.5v1H2V3z'/%3E%3Cpath d='M2 5h12v8a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5z'/%3E%3C/svg%3E");
}
.bi-speedometer-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' viewBox='0 0 16 16'%3E%3Cpath d='M1 11a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1V7a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v4zm5 0a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H7a1 1 0 0 0-1 1v8zm5 0a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1V9a1 1 0 0 0-1-1h-2a1 1 0 0 0-1 1v2z'/%3E%3C/svg%3E");
}
.bi-table-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' viewBox='0 0 16 16'%3E%3Cpath d='M1 2a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V2zm1 1v2h12V3H2zm12 3H2v2h12V6zm0 3H2v2h12V9zm0 3H2v1h12v-1z'/%3E%3C/svg%3E");
}
.nav-item { .nav-item {
font-size: 0.9rem; font-size: 0.9rem;
padding-bottom: 0.5rem; padding-bottom: 0.5rem;

View File

@@ -0,0 +1,834 @@
@using System.Text.Json
@using Microsoft.AspNetCore.Components
@using Microsoft.AspNetCore.Components.Rendering
@using Microsoft.AspNetCore.Components.Forms
@using DevExpress.Blazor
@using DevExpress.Data.Filtering
@inject MassDataApiClient Api
@inject LayoutApiClient LayoutApi
@inject IJSRuntime JsRuntime
<style>
.action-panel { margin-bottom: 16px; }
.grid-section { margin-top: 12px; }
.pager-container {
display: flex;
justify-content: center;
margin-top: 12px;
margin-bottom: 16px;
}
.page-size-selector {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: nowrap;
}
.page-size-label {
white-space: nowrap;
}
.page-size-combo {
width: 13ch;
min-width: 13ch;
max-width: 13ch;
}
.page-size-combo input {
text-align: left;
}
.massdata-edit-popup {
min-width: 720px;
}
.band-editor {
display: grid;
gap: 12px;
margin-bottom: 16px;
}
.band-controls {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
}
.band-row {
display: flex;
gap: 8px;
align-items: center;
}
.band-columns {
max-width: 720px;
}
.loading-container {
min-height: 160px;
display: flex;
align-items: center;
justify-content: center;
}
</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>
}
@if (!hasLoaded || isLoading)
{
<div class="loading-container">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Lade...</span>
</div>
</div>
}
else if (items.Count == 0)
{
<p>Keine Eintr&#228;ge vorhanden.</p>
}
else
{
<div class="mb-3 page-size-selector">
<span class="page-size-label">Datens&#228;tze je Seite:</span>
<DxComboBox Data="@pageSizeOptions"
TData="PageSizeOption"
TValue="int?"
TextFieldName="Text"
ValueFieldName="Value"
Value="@pageSize"
ValueChanged="OnPageSizeChanged"
CssClass="page-size-combo" />
</div>
<div class="band-editor">
<div class="band-controls">
<DxButton Text="Band hinzuf&#252;gen" Click="AddBand" />
<DxButton Text="Layout speichern" Click="SaveLayoutAsync" Enabled="@CanSaveBandLayout" />
<DxButton Text="Band-Layout zur&#252;cksetzen" Click="ResetBandLayoutAsync" />
</div>
@foreach (var band in bandLayout.Bands)
{
<div class="band-row">
<DxTextBox Text="@band.Caption" TextChanged="@(value => UpdateBandCaption(band, value))" />
<DxButton Text="Entfernen" Click="@(() => RemoveBand(band))" />
</div>
}
<DxFormLayout CssClass="band-columns" ColCount="2">
@foreach (var column in columnDefinitions)
{
<DxFormLayoutItem Caption="@column.Caption">
<DxComboBox Data="@bandOptions"
TData="BandOption"
TValue="string"
TextFieldName="Caption"
ValueFieldName="Id"
Value="@GetColumnBand(column.FieldName)"
ValueChanged="@(value => UpdateColumnBand(column.FieldName, value))"
Width="100%" />
</DxFormLayoutItem>
}
</DxFormLayout>
</div>
<div class="grid-section">
<DxGrid Data="@items"
TItem="MassDataReadDto"
KeyFieldName="@nameof(MassDataReadDto.Id)"
SizeMode="@_sizeMode"
ShowGroupPanel="true"
ShowGroupedColumns="true"
AllowGroup="true"
FilterMenuButtonDisplayMode="GridFilterMenuButtonDisplayMode.Always"
AllowColumnResize="true"
ColumnResizeMode="GridColumnResizeMode.ColumnsContainer"
AllowColumnReorder="true"
PagerVisible="false"
PageSize="@(pageSize ?? 100)"
CssClass="mb-3 massdata-grid"
EditMode="GridEditMode.PopupEditForm"
PopupEditFormHeaderText="@popupHeaderText"
CustomizeEditModel="OnCustomizeEditModel"
EditModelSaving="OnEditModelSaving"
DataItemDeleting="OnDataItemDeleting"
FocusedRowEnabled="true"
@bind-FocusedRowKey="focusedRowKey"
@ref="gridRef">
<ToolbarTemplate>
<DxToolbar>
<DxToolbarItem Alignment="ToolbarItemAlignment.Right">
<Template Context="_">
<DxDropDownButton Text="@FormatSizeText(_sizeMode)"
RenderStyle="ButtonRenderStyle.Secondary"
RenderStyleMode="ButtonRenderStyleMode.Text"
ItemClick="OnSizeChange">
<Items>
@foreach (var size in _sizeModes)
{
<DxDropDownButtonItem Text="@FormatSizeText(size)" Id="@size.ToString()" />
}
</Items>
</DxDropDownButton>
</Template>
</DxToolbarItem>
</DxToolbar>
</ToolbarTemplate>
<Columns>
@RenderColumns()
</Columns>
<EditFormTemplate Context="editFormContext">
@{ SetEditContext(editFormContext.EditContext); var editModel = (MassDataEditModel)editFormContext.EditModel; SetPopupHeaderText(editModel.IsNew); }
<DxFormLayout ColCount="2">
<DxFormLayoutItem Caption="CustomerName">
<DxTextBox @bind-Text="editModel.CustomerName" Width="100%" />
</DxFormLayoutItem>
<DxFormLayoutItem Caption="Amount">
<DxTextBox @bind-Text="editModel.AmountText" Width="100%" />
</DxFormLayoutItem>
<DxFormLayoutItem Caption="Category">
<DxTextBox @bind-Text="editModel.Category" Width="100%" ReadOnly="@(!editModel.IsNew)" />
</DxFormLayoutItem>
<DxFormLayoutItem Caption="Status">
<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>
</DxGrid>
@if (pageCount > 1)
{
<div class="pager-container">
<DxPager PageCount="@pageCount" ActivePageIndex="@pageIndex" ActivePageIndexChanged="OnPageChanged" />
</div>
}
</div>
}
@code {
private List<MassDataReadDto> items = new();
private bool isLoading;
private bool hasLoaded;
private string? errorMessage;
private string? infoMessage;
private int pageIndex;
private int pageCount = 1;
private int? pageSize = 100;
private string popupHeaderText = "Edit";
private EditContext? editContext;
private ValidationMessageStore? validationMessageStore;
private IGrid? gridRef;
private int? focusedRowKey;
private const string LayoutType = "GRID_BANDS";
private const string LayoutKey = "MassDataGrid";
private const string LayoutUserStorageKey = "layoutUser";
private string? layoutUser;
private BandLayout bandLayout = new();
private Dictionary<string, string> columnBandAssignments = new();
private List<BandOption> bandOptions = new();
private Dictionary<string, ColumnDefinition> columnLookup = new();
private readonly JsonSerializerOptions jsonOptions = new(JsonSerializerDefaults.Web);
private List<ColumnDefinition> columnDefinitions = new()
{
new() { FieldName = nameof(MassDataReadDto.Id), Caption = "Id", Width = "90px", ReadOnly = true, FilterType = ColumnFilterType.Text },
new() { FieldName = nameof(MassDataReadDto.CustomerName), Caption = "CustomerName", FilterType = ColumnFilterType.Text },
new() { FieldName = nameof(MassDataReadDto.Amount), Caption = "Amount", DisplayFormat = "c2", FilterType = ColumnFilterType.Text },
new() { FieldName = nameof(MassDataReadDto.Category), Caption = "Category", ReadOnly = true, FilterType = ColumnFilterType.Text },
new() { FieldName = nameof(MassDataReadDto.StatusFlag), Caption = "Status", ReadOnly = true, FilterType = ColumnFilterType.Bool },
new() { FieldName = nameof(MassDataReadDto.AddedWhen), Caption = "Added", ReadOnly = true, FilterType = ColumnFilterType.Date },
new() { FieldName = nameof(MassDataReadDto.ChangedWhen), Caption = "Changed", ReadOnly = true, FilterType = ColumnFilterType.Date }
};
private bool CanSaveBandLayout => !string.IsNullOrWhiteSpace(layoutUser);
private readonly List<PageSizeOption> pageSizeOptions = new()
{
new() { Value = 100, Text = "100" },
new() { Value = 1000, Text = "1.000" },
new() { Value = 10000, Text = "10.000" },
new() { Value = 100000, Text = "100.000" },
new() { Value = null, Text = "Alle" }
};
private readonly List<ProcedureOption> procedureOptions = new()
{
new() { Value = 0, Text = "PRMassdata_UpsertByCustomerName" }
};
private bool gridLayoutApplied;
private DevExpress.Blazor.SizeMode _sizeMode = DevExpress.Blazor.SizeMode.Medium;
private static readonly List<DevExpress.Blazor.SizeMode> _sizeModes =
Enum.GetValues<DevExpress.Blazor.SizeMode>().ToList();
private string FormatSizeText(DevExpress.Blazor.SizeMode size) => size switch
{
DevExpress.Blazor.SizeMode.Small => "Klein",
DevExpress.Blazor.SizeMode.Medium => "Mittel",
DevExpress.Blazor.SizeMode.Large => "Groß",
_ => size.ToString()
};
private void OnSizeChange(DropDownButtonItemClickEventArgs args)
{
_sizeMode = Enum.Parse<DevExpress.Blazor.SizeMode>(args.ItemInfo.Id);
}
protected override async Task OnInitializedAsync()
{
columnLookup = columnDefinitions.ToDictionary(column => column.FieldName, StringComparer.OrdinalIgnoreCase);
await EnsureLayoutUserAsync();
await LoadBandLayoutAsync();
await LoadPage(0);
}
private async Task LoadPage(int page)
{
isLoading = true;
errorMessage = null;
try
{
var total = await Api.GetCountAsync();
var effectivePageSize = pageSize ?? (total == 0 ? 1 : total);
pageCount = Math.Max(1, (int)Math.Ceiling(total / (double)effectivePageSize));
pageIndex = Math.Clamp(page, 0, pageCount - 1);
var skip = pageSize.HasValue ? pageIndex * pageSize.Value : 0;
items = await Api.GetAllAsync(skip, pageSize);
}
catch (Exception ex)
{
errorMessage = $"MassData konnten nicht geladen werden: {ex.Message}";
}
finally
{
isLoading = false;
hasLoaded = true;
StateHasChanged();
}
}
private async Task OnPageChanged(int index)
{
await LoadPage(index);
}
private async Task OnPageSizeChanged(int? size)
{
pageSize = size;
await LoadPage(0);
}
private async Task EnsureLayoutUserAsync()
{
layoutUser = await JsRuntime.InvokeAsync<string?>("localStorage.getItem", LayoutUserStorageKey);
if (string.IsNullOrWhiteSpace(layoutUser))
{
layoutUser = Guid.NewGuid().ToString("N");
await JsRuntime.InvokeVoidAsync("localStorage.setItem", LayoutUserStorageKey, layoutUser);
}
}
private async Task LoadBandLayoutAsync()
{
if (string.IsNullOrWhiteSpace(layoutUser))
{
bandLayout = new BandLayout();
UpdateBandOptions();
return;
}
var stored = await LayoutApi.GetAsync(LayoutType, LayoutKey, layoutUser);
if (stored != null && !string.IsNullOrWhiteSpace(stored.LayoutData))
{
var parsed = JsonSerializer.Deserialize<BandLayout>(stored.LayoutData, jsonOptions);
bandLayout = NormalizeBandLayout(parsed);
}
else
{
bandLayout = new BandLayout();
}
columnBandAssignments = BuildAssignmentsFromLayout(bandLayout);
ApplyColumnLayoutFromStorage();
UpdateBandOptions();
}
private async Task SaveLayoutAsync()
{
if (string.IsNullOrWhiteSpace(layoutUser))
{
return;
}
try
{
CaptureColumnLayoutFromGrid();
var layoutData = JsonSerializer.Serialize(bandLayout, jsonOptions);
await LayoutApi.UpsertAsync(new LayoutDto
{
LayoutType = LayoutType,
LayoutKey = LayoutKey,
UserName = layoutUser,
LayoutData = layoutData
});
infoMessage = "Layout gespeichert.";
errorMessage = null;
}
catch (Exception ex)
{
errorMessage = $"Layout konnte nicht gespeichert werden: {ex.Message}";
}
}
private void CaptureColumnLayoutFromGrid()
{
if (gridRef == null)
{
return;
}
var layout = gridRef.SaveLayout();
bandLayout.GridLayout = layout;
var orderedColumns = layout.Columns
.Where(column => !string.IsNullOrWhiteSpace(column.FieldName))
.OrderBy(column => column.VisibleIndex)
.ToList();
bandLayout.ColumnOrder = orderedColumns
.Select(column => column.FieldName)
.ToList();
bandLayout.ColumnWidths = orderedColumns
.Where(column => !string.IsNullOrWhiteSpace(column.Width))
.ToDictionary(column => column.FieldName, column => column.Width, StringComparer.OrdinalIgnoreCase);
}
private async Task ResetBandLayoutAsync()
{
if (string.IsNullOrWhiteSpace(layoutUser))
{
return;
}
await LayoutApi.DeleteAsync(LayoutType, LayoutKey, layoutUser);
bandLayout = new BandLayout();
columnBandAssignments.Clear();
UpdateBandOptions();
infoMessage = "Band-Layout zur&#252;ckgesetzt.";
}
private void ApplyColumnLayoutFromStorage()
{
foreach (var column in columnDefinitions)
{
if (bandLayout.ColumnWidths.TryGetValue(column.FieldName, out var width) && !string.IsNullOrWhiteSpace(width))
{
column.Width = width;
}
}
columnLookup = columnDefinitions.ToDictionary(column => column.FieldName, StringComparer.OrdinalIgnoreCase);
}
private void AddBand()
{
bandLayout.Bands.Add(new BandDefinition
{
Id = Guid.NewGuid().ToString("N"),
Caption = "Band"
});
UpdateBandOptions();
}
private void RemoveBand(BandDefinition band)
{
bandLayout.Bands.Remove(band);
var removedColumns = columnBandAssignments.Where(pair => pair.Value == band.Id)
.Select(pair => pair.Key)
.ToList();
foreach (var column in removedColumns)
{
columnBandAssignments.Remove(column);
}
UpdateBandOptions();
SyncBandsFromAssignments();
}
private void UpdateBandCaption(BandDefinition band, string value)
{
band.Caption = value;
UpdateBandOptions();
}
private void UpdateColumnBand(string fieldName, string? bandId)
{
if (string.IsNullOrWhiteSpace(bandId))
{
columnBandAssignments.Remove(fieldName);
}
else
{
columnBandAssignments[fieldName] = bandId;
}
SyncBandsFromAssignments();
}
private string GetColumnBand(string fieldName)
{
return columnBandAssignments.TryGetValue(fieldName, out var bandId) ? bandId : string.Empty;
}
private void SyncBandsFromAssignments()
{
foreach (var band in bandLayout.Bands)
{
band.Columns = columnDefinitions
.Where(column => columnBandAssignments.TryGetValue(column.FieldName, out var bandId) && bandId == band.Id)
.Select(column => column.FieldName)
.ToList();
}
StateHasChanged();
}
private void UpdateBandOptions()
{
bandOptions = new List<BandOption> { new() { Id = string.Empty, Caption = "Ohne Band" } };
bandOptions.AddRange(bandLayout.Bands.Select(band => new BandOption { Id = band.Id, Caption = band.Caption }));
}
private Dictionary<string, string> BuildAssignmentsFromLayout(BandLayout layout)
{
var assignments = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
foreach (var band in layout.Bands)
{
foreach (var column in band.Columns)
{
assignments[column] = band.Id;
}
}
return assignments;
}
private BandLayout NormalizeBandLayout(BandLayout? layout)
{
layout ??= new BandLayout();
layout.Bands ??= new List<BandDefinition>();
layout.ColumnOrder ??= new List<string>();
layout.ColumnWidths ??= new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
foreach (var band in layout.Bands)
{
if (string.IsNullOrWhiteSpace(band.Id))
{
band.Id = Guid.NewGuid().ToString("N");
}
if (string.IsNullOrWhiteSpace(band.Caption))
{
band.Caption = "Band";
}
band.Columns = band.Columns?.Where(columnLookup.ContainsKey).ToList() ?? new List<string>();
}
return layout;
}
private RenderFragment RenderColumns() => builder =>
{
var seq = 0;
builder.OpenComponent<DxGridCommandColumn>(seq++);
builder.AddAttribute(seq++, "Width", "120px");
builder.CloseComponent();
var grouped = bandLayout.Bands.SelectMany(band => band.Columns).ToHashSet(StringComparer.OrdinalIgnoreCase);
foreach (var column in columnDefinitions.Where(column => !grouped.Contains(column.FieldName)))
{
BuildDataColumn(builder, ref seq, column);
}
foreach (var band in bandLayout.Bands)
{
if (band.Columns.Count == 0)
{
continue;
}
builder.OpenComponent<DxGridBandColumn>(seq++);
builder.AddAttribute(seq++, "Caption", band.Caption);
builder.AddAttribute(seq++, "Columns", (RenderFragment)(bandBuilder =>
{
var bandSeq = 0;
foreach (var columnName in band.Columns)
{
if (columnLookup.TryGetValue(columnName, out var column))
{
BuildDataColumn(bandBuilder, ref bandSeq, column);
}
}
}));
builder.CloseComponent();
}
};
private void BuildDataColumn(RenderTreeBuilder builder, ref int seq, ColumnDefinition column)
{
builder.OpenComponent<DxGridDataColumn>(seq++);
builder.AddAttribute(seq++, "FieldName", column.FieldName);
builder.AddAttribute(seq++, "Caption", column.Caption);
if (!string.IsNullOrWhiteSpace(column.Width))
{
builder.AddAttribute(seq++, "Width", column.Width);
}
if (!string.IsNullOrWhiteSpace(column.DisplayFormat))
{
builder.AddAttribute(seq++, "DisplayFormat", column.DisplayFormat);
}
if (column.ReadOnly)
{
builder.AddAttribute(seq++, "ReadOnly", true);
}
builder.CloseComponent();
}
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 async Task 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;
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&#252;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 = amount,
Category = editModel.Category,
StatusFlag = editModel.StatusFlag
};
try
{
var saved = await Api.UpsertAsync(dto);
infoMessage = editModel.IsNew ? "MassData angelegt." : "MassData aktualisiert.";
focusedRowKey = saved.Id;
await LoadPage(pageIndex);
}
catch (Exception ex)
{
errorMessage = $"Fehler beim Speichern: {ex.Message}";
e.Cancel = true;
}
}
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;
infoMessage = "L&#246;schen ist aktuell noch nicht verf&#252;gbar.";
e.Cancel = true;
return Task.CompletedTask;
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (!gridLayoutApplied && gridRef != null && bandLayout.GridLayout != null)
{
gridRef.LoadLayout(bandLayout.GridLayout);
gridLayoutApplied = true;
await InvokeAsync(StateHasChanged);
}
}
private sealed class BandLayout
{
public List<BandDefinition> Bands { get; set; } = new();
public List<string> ColumnOrder { get; set; } = new();
public Dictionary<string, string?> ColumnWidths { get; set; } = new(StringComparer.OrdinalIgnoreCase);
public GridPersistentLayout? GridLayout { get; set; }
}
private sealed class BandDefinition
{
public string Id { get; set; } = string.Empty;
public string Caption { get; set; } = string.Empty;
public List<string> Columns { get; set; } = new();
}
private sealed class BandOption
{
public string Id { get; set; } = string.Empty;
public string Caption { get; set; } = string.Empty;
}
private sealed class ColumnDefinition
{
public string FieldName { get; init; } = string.Empty;
public string Caption { get; init; } = string.Empty;
public string? Width { get; set; }
public string? DisplayFormat { get; init; }
public bool ReadOnly { get; init; }
public ColumnFilterType FilterType { get; init; }
}
private enum ColumnFilterType
{
Text,
Bool,
Date
}
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; }
public string Text { get; set; } = string.Empty;
}
private sealed class PageSizeOption
{
public int? Value { get; set; }
public string Text { get; set; } = string.Empty;
}
private sealed class FilterOperatorOption
{
public string Value { get; set; } = string.Empty;
public string Text { get; set; } = string.Empty;
}
}

View File

@@ -1,19 +0,0 @@
@page "/counter"
@rendermode InteractiveServer
<PageTitle>Counter</PageTitle>
<h1>Counter</h1>
<p role="status">Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code {
private int currentCount = 0;
private void IncrementCount()
{
currentCount++;
}
}

View File

@@ -1,5 +1,6 @@
@page "/dashboard" @page "/dashboard"
@page "/dashboards/{DashboardId?}" @page "/dashboards/{DashboardId?}"
@implements IAsyncDisposable
@inject Microsoft.Extensions.Configuration.IConfiguration Configuration @inject Microsoft.Extensions.Configuration.IConfiguration Configuration
@inject NavigationManager Navigation @inject NavigationManager Navigation
@inject DashboardApiClient DashboardApi @inject DashboardApiClient DashboardApi
@@ -57,7 +58,7 @@
{ {
@foreach (var dashboard in dashboards) @foreach (var dashboard in dashboards)
{ {
<NavLink class="dashboard-nav-link" href="@($"dashboards/{dashboard.Id}")">@dashboard.Name</NavLink> <NavLink class="dashboard-nav-link" href="@($"dashboards/{dashboard.Id}?mode={(IsDesigner ? "designer" : "viewer")}")">@dashboard.Name</NavLink>
} }
} }
</aside> </aside>
@@ -77,19 +78,38 @@
[SupplyParameterFromQuery] public string? Mode { get; set; } [SupplyParameterFromQuery] public string? Mode { get; set; }
private readonly List<DashboardInfoDto> dashboards = new(); private readonly List<DashboardInfoDto> dashboards = new();
private HubConnection? _hubConnection;
private bool IsDesigner => !string.Equals(Mode, "viewer", StringComparison.OrdinalIgnoreCase); private bool IsDesigner => !string.Equals(Mode, "viewer", StringComparison.OrdinalIgnoreCase);
private WorkingMode CurrentMode => IsDesigner ? WorkingMode.Designer : WorkingMode.ViewerOnly; private WorkingMode CurrentMode => IsDesigner ? WorkingMode.Designer : WorkingMode.ViewerOnly;
private string SelectedDashboardId { get; set; } = ""; 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 => $"{Configuration["ApiBaseUrl"]?.TrimEnd('/')}/api/dashboard";
private string HubEndpoint => $"{Configuration["ApiBaseUrl"]?.TrimEnd('/')}/hubs/dashboards";
protected override async Task OnInitializedAsync()
{
await RefreshDashboards();
_hubConnection = new HubConnectionBuilder()
.WithUrl(HubEndpoint)
.WithAutomaticReconnect()
.Build();
_hubConnection.On("DashboardsChanged", async () =>
{
await RefreshDashboards();
});
await _hubConnection.StartAsync();
}
protected override async Task OnParametersSetAsync() protected override async Task OnParametersSetAsync()
{ {
if (dashboards.Count == 0) if (dashboards.Count == 0)
{ {
dashboards.AddRange(await DashboardApi.GetAllAsync()); await RefreshDashboards();
} }
var requestedId = string.IsNullOrWhiteSpace(DashboardId) || string.Equals(DashboardId, "default", StringComparison.OrdinalIgnoreCase) var requestedId = string.IsNullOrWhiteSpace(DashboardId) || string.Equals(DashboardId, "default", StringComparison.OrdinalIgnoreCase)
@@ -119,4 +139,25 @@
var targetMode = IsDesigner ? "viewer" : "designer"; var targetMode = IsDesigner ? "viewer" : "designer";
Navigation.NavigateTo($"dashboards/{SelectedDashboardId}?mode={targetMode}", replace: true); Navigation.NavigateTo($"dashboards/{SelectedDashboardId}?mode={targetMode}", replace: true);
} }
private async Task RefreshDashboards()
{
var latest = await DashboardApi.GetAllAsync();
if (latest.Count == dashboards.Count && latest.All(d => dashboards.Any(x => x.Id == d.Id && x.Name == d.Name)))
{
return;
}
dashboards.Clear();
dashboards.AddRange(latest);
await InvokeAsync(StateHasChanged);
}
public async ValueTask DisposeAsync()
{
if (_hubConnection != null)
{
await _hubConnection.DisposeAsync();
}
}
} }

View File

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

View File

@@ -1,64 +0,0 @@
@page "/weather"
@attribute [StreamRendering]
<PageTitle>Weather</PageTitle>
<h1>Weather</h1>
<p>This component demonstrates showing data.</p>
@if (forecasts == null)
{
<p><em>Loading...</em></p>
}
else
{
<table class="table">
<thead>
<tr>
<th>Date</th>
<th>Temp. (C)</th>
<th>Temp. (F)</th>
<th>Summary</th>
</tr>
</thead>
<tbody>
@foreach (var forecast in forecasts)
{
<tr>
<td>@forecast.Date.ToShortDateString()</td>
<td>@forecast.TemperatureC</td>
<td>@forecast.TemperatureF</td>
<td>@forecast.Summary</td>
</tr>
}
</tbody>
</table>
}
@code {
private WeatherForecast[]? forecasts;
protected override async Task OnInitializedAsync()
{
// Simulate asynchronous loading to demonstrate streaming rendering
await Task.Delay(500);
var startDate = DateOnly.FromDateTime(DateTime.Now);
var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" };
forecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = startDate.AddDays(index),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = summaries[Random.Shared.Next(summaries.Length)]
}).ToArray();
}
private class WeatherForecast
{
public DateOnly Date { get; set; }
public int TemperatureC { get; set; }
public string? Summary { get; set; }
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
}

View File

@@ -6,6 +6,7 @@
@using static Microsoft.AspNetCore.Components.Web.RenderMode @using static Microsoft.AspNetCore.Components.Web.RenderMode
@using Microsoft.AspNetCore.Components.Web.Virtualization @using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop @using Microsoft.JSInterop
@using Microsoft.AspNetCore.SignalR.Client
@using DbFirst.BlazorWebApp @using DbFirst.BlazorWebApp
@using DbFirst.BlazorWebApp.Components @using DbFirst.BlazorWebApp.Components
@using DbFirst.BlazorWebApp.Models @using DbFirst.BlazorWebApp.Models

View File

@@ -12,5 +12,9 @@
<PackageReference Include="DevExpress.Blazor.Themes" Version="25.2.3" /> <PackageReference Include="DevExpress.Blazor.Themes" Version="25.2.3" />
<PackageReference Include="DevExpress.Blazor.Themes.Fluent" Version="25.2.3" /> <PackageReference Include="DevExpress.Blazor.Themes.Fluent" Version="25.2.3" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<PackageReference Include="DevExpress.Blazor" Version="25.2.3" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="8.0.22" />
</ItemGroup>
</Project> </Project>

View File

@@ -0,0 +1,9 @@
namespace DbFirst.BlazorWebApp.Models;
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,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

@@ -8,7 +8,8 @@ var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorComponents() builder.Services.AddRazorComponents()
.AddInteractiveServerComponents(); .AddInteractiveServerComponents();
builder.Services.AddDevExpressBlazor(); builder.Services.AddDevExpressBlazor(options => options.BootstrapVersion = BootstrapVersion.v5);
builder.Services.AddScoped<ThemeState>();
var apiBaseUrl = builder.Configuration["ApiBaseUrl"]; var apiBaseUrl = builder.Configuration["ApiBaseUrl"];
if (!string.IsNullOrWhiteSpace(apiBaseUrl)) if (!string.IsNullOrWhiteSpace(apiBaseUrl))
@@ -21,11 +22,21 @@ if (!string.IsNullOrWhiteSpace(apiBaseUrl))
{ {
client.BaseAddress = new Uri(apiBaseUrl); 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 else
{ {
builder.Services.AddHttpClient<CatalogApiClient>(); builder.Services.AddHttpClient<CatalogApiClient>();
builder.Services.AddHttpClient<DashboardApiClient>(); builder.Services.AddHttpClient<DashboardApiClient>();
builder.Services.AddHttpClient<MassDataApiClient>();
builder.Services.AddHttpClient<LayoutApiClient>();
} }
var app = builder.Build(); var app = builder.Build();

View File

@@ -0,0 +1,64 @@
using System.Net.Http.Json;
using DbFirst.BlazorWebApp.Models;
namespace DbFirst.BlazorWebApp.Services;
public class LayoutApiClient
{
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)
{
var url = $"{Endpoint}?layoutType={Uri.EscapeDataString(layoutType)}&layoutKey={Uri.EscapeDataString(layoutKey)}&userName={Uri.EscapeDataString(userName)}";
var response = await _httpClient.GetAsync(url);
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
{
return null;
}
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<LayoutDto>();
}
public async Task<LayoutDto> UpsertAsync(LayoutDto dto)
{
var response = await _httpClient.PostAsJsonAsync(Endpoint, dto);
if (!response.IsSuccessStatusCode)
{
var detail = await ReadErrorAsync(response);
throw new InvalidOperationException(detail);
}
var payload = await response.Content.ReadFromJsonAsync<LayoutDto>();
return payload ?? dto;
}
private static async Task<string> ReadErrorAsync(HttpResponseMessage response)
{
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 response = await _httpClient.DeleteAsync(url);
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
{
return;
}
response.EnsureSuccessStatusCode();
}
}

View File

@@ -0,0 +1,63 @@
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 query = new List<string>();
if (skip.HasValue)
{
query.Add($"skip={skip.Value}");
}
if (take.HasValue)
{
query.Add($"take={take.Value}");
}
var url = query.Count == 0 ? Endpoint : $"{Endpoint}?{string.Join("&", query)}";
var result = await _httpClient.GetFromJsonAsync<List<MassDataReadDto>>(url);
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();
}
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

@@ -0,0 +1,35 @@
using DevExpress.Blazor;
namespace DbFirst.BlazorWebApp.Services;
public class ThemeState
{
private readonly IThemeChangeService themeChangeService;
public ThemeState(IThemeChangeService themeChangeService)
{
this.themeChangeService = themeChangeService;
}
public bool IsDarkMode { get; private set; }
public event Action? OnChange;
public void SetDarkMode(bool isDarkMode)
{
if (IsDarkMode == isDarkMode)
{
return;
}
IsDarkMode = isDarkMode;
var theme = Themes.Fluent.Clone(properties =>
{
properties.Mode = isDarkMode ? ThemeMode.Dark : ThemeMode.Light;
properties.ApplyToPageElements = true;
});
themeChangeService.SetTheme(theme);
OnChange?.Invoke();
}
}

View File

@@ -1,11 +1,25 @@
:root {
--global-size: 16px;
}
html, body { html, body {
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
font-size: var(--global-size);
}
.app-dark {
background-color: #1b1b1b;
color: #f1f1f1;
} }
a, .btn-link { a, .btn-link {
color: #006bb7; color: #006bb7;
} }
.app-dark a, .app-dark .btn-link {
color: #6cb6ff;
}
.btn-primary { .btn-primary {
color: #fff; color: #fff;
background-color: #1b6ec2; background-color: #1b6ec2;
@@ -37,7 +51,7 @@ h1:focus {
} }
.blazor-error-boundary { .blazor-error-boundary {
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA9NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDg2IDY2LjAxODMgMjYzLjU4NiA2Ni4wMTgzWk0yNjMuNTc2IDg2LjA1NDdDMjYxLjA0OSA4Ni4wNTQ3IDI1OS43ODUgODcuMzAwNSAxNTEuMDIyIDg5Ljc5MjEgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
padding: 1rem 1rem 1rem 3.7rem; padding: 1rem 1rem 1rem 3.7rem;
color: white; color: white;
} }

View File

@@ -0,0 +1,3 @@
window.setSize = function (fontSize) {
document.documentElement.style.setProperty('--global-size', fontSize);
};

View File

@@ -0,0 +1,12 @@
namespace DbFirst.Domain.Entities;
public class Massdata
{
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,15 @@
namespace DbFirst.Domain.Entities;
public class SmfLayout
{
public long Guid { get; set; }
public bool Active { get; set; }
public string LayoutType { get; set; } = string.Empty;
public string LayoutKey { get; set; } = string.Empty;
public string UserName { get; set; } = string.Empty;
public byte[] LayoutData { get; set; } = Array.Empty<byte>();
public string AddedWho { get; set; } = string.Empty;
public DateTime AddedWhen { get; set; }
public string? ChangedWho { get; set; }
public DateTime? ChangedWhen { get; set; }
}

View File

@@ -15,6 +15,7 @@ public partial class ApplicationDbContext : DbContext
} }
public virtual DbSet<VwmyCatalog> VwmyCatalogs { get; set; } public virtual DbSet<VwmyCatalog> VwmyCatalogs { get; set; }
public virtual DbSet<SmfLayout> SmfLayouts { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder)
{ {
@@ -51,6 +52,37 @@ public partial class ApplicationDbContext : DbContext
.HasColumnName(catCfg.ChangedWhoColumnName); .HasColumnName(catCfg.ChangedWhoColumnName);
}); });
modelBuilder.Entity<SmfLayout>(entity =>
{
entity.HasKey(e => e.Guid);
entity.ToTable("TBDD_SMF_LAYOUT", tb => tb.HasTrigger("TBDD_SMF_LAYOUT_AFT_UPD"));
entity.Property(e => e.Guid).HasColumnName("GUID");
entity.Property(e => e.Active).HasColumnName("ACTIVE");
entity.Property(e => e.LayoutType)
.HasMaxLength(50)
.HasColumnName("LAYOUT_TYPE");
entity.Property(e => e.LayoutKey)
.HasMaxLength(150)
.HasColumnName("LAYOUT_KEY");
entity.Property(e => e.UserName)
.HasMaxLength(50)
.HasColumnName("USER_NAME");
entity.Property(e => e.LayoutData).HasColumnName("LAYOUT_DATA");
entity.Property(e => e.AddedWho)
.HasMaxLength(50)
.HasColumnName("ADDED_WHO");
entity.Property(e => e.AddedWhen)
.HasColumnType("datetime")
.HasColumnName("ADDED_WHEN");
entity.Property(e => e.ChangedWho)
.HasMaxLength(50)
.HasColumnName("CHANGED_WHO");
entity.Property(e => e.ChangedWhen)
.HasColumnType("datetime")
.HasColumnName("CHANGED_WHEN");
});
OnModelCreatingPartial(modelBuilder); OnModelCreatingPartial(modelBuilder);
} }

View File

@@ -11,6 +11,10 @@ public static class DependencyInjection
services.Configure<TableConfigurations>(configuration.GetSection("TableConfigurations")); services.Configure<TableConfigurations>(configuration.GetSection("TableConfigurations"));
services.AddDbContext<ApplicationDbContext>(options => services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(configuration.GetConnectionString("DefaultConnection"))); options.UseSqlServer(configuration.GetConnectionString("DefaultConnection")));
services.AddDbContext<MassDataDbContext>(options =>
options.UseSqlServer(configuration.GetConnectionString("MassDataConnection")));
return services; return services;
} }
} }

View File

@@ -0,0 +1,44 @@
using DbFirst.Domain.Entities;
using Microsoft.EntityFrameworkCore;
namespace DbFirst.Infrastructure;
public class MassDataDbContext : DbContext
{
public MassDataDbContext(DbContextOptions<MassDataDbContext> options)
: base(options)
{
}
public virtual DbSet<Massdata> Massdata { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Massdata>(entity =>
{
entity.HasKey(e => e.Id);
entity.ToTable("MASSDATA");
entity.Property(e => e.Id).HasColumnName("ID");
entity.Property(e => e.CustomerName)
.HasMaxLength(200)
.IsUnicode(false)
.HasColumnName("CustomerName");
entity.Property(e => e.Amount)
.HasColumnType("decimal(12,2)")
.HasColumnName("Amount");
entity.Property(e => e.Category)
.HasMaxLength(100)
.IsUnicode(false)
.HasColumnName("Category");
entity.Property(e => e.StatusFlag)
.HasColumnName("StatusFlag");
entity.Property(e => e.AddedWhen)
.HasColumnType("datetime")
.HasColumnName("ADDED_WHEN");
entity.Property(e => e.ChangedWhen)
.HasColumnType("datetime")
.HasColumnName("CHANGED_WHEN");
});
}
}

View File

@@ -0,0 +1,66 @@
using DbFirst.Application.Repositories;
using DbFirst.Domain.Entities;
using Microsoft.EntityFrameworkCore;
namespace DbFirst.Infrastructure.Repositories;
public class LayoutRepository : ILayoutRepository
{
private readonly ApplicationDbContext _db;
public LayoutRepository(ApplicationDbContext db)
{
_db = db;
}
public async Task<SmfLayout?> GetAsync(string layoutType, string layoutKey, string userName, CancellationToken cancellationToken = default)
{
return await _db.SmfLayouts.AsNoTracking()
.FirstOrDefaultAsync(x => x.LayoutType == layoutType && x.LayoutKey == layoutKey && x.UserName == userName, cancellationToken);
}
public async Task<SmfLayout> UpsertAsync(string layoutType, string layoutKey, string userName, byte[] layoutData, CancellationToken cancellationToken = default)
{
var entity = await _db.SmfLayouts
.FirstOrDefaultAsync(x => x.LayoutType == layoutType && x.LayoutKey == layoutKey && x.UserName == userName, cancellationToken);
if (entity == null)
{
entity = new SmfLayout
{
Active = true,
LayoutType = layoutType,
LayoutKey = layoutKey,
UserName = userName,
LayoutData = layoutData,
AddedWho = userName,
AddedWhen = DateTime.Now
};
_db.SmfLayouts.Add(entity);
}
else
{
entity.Active = true;
entity.LayoutData = layoutData;
entity.ChangedWho = userName;
}
await _db.SaveChangesAsync(cancellationToken);
return entity;
}
public async Task<bool> DeleteAsync(string layoutType, string layoutKey, string userName, CancellationToken cancellationToken = default)
{
var entity = await _db.SmfLayouts
.FirstOrDefaultAsync(x => x.LayoutType == layoutType && x.LayoutKey == layoutKey && x.UserName == userName, cancellationToken);
if (entity == null)
{
return false;
}
_db.SmfLayouts.Remove(entity);
await _db.SaveChangesAsync(cancellationToken);
return true;
}
}

View File

@@ -0,0 +1,71 @@
using System.Data;
using DbFirst.Application.Repositories;
using DbFirst.Domain.Entities;
using Microsoft.Data.SqlClient;
using Microsoft.EntityFrameworkCore;
namespace DbFirst.Infrastructure.Repositories;
public class MassDataRepository : IMassDataRepository
{
private readonly MassDataDbContext _db;
public MassDataRepository(MassDataDbContext db)
{
_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);
}
public async Task<Massdata?> GetByCustomerNameAsync(string customerName, CancellationToken cancellationToken = default)
{
return await _db.Massdata.AsNoTracking()
.FirstOrDefaultAsync(x => x.CustomerName == customerName, cancellationToken);
}
public async Task<List<Massdata>> GetAllAsync(int? skip = null, int? take = null, CancellationToken cancellationToken = default)
{
var query = _db.Massdata.AsNoTracking().OrderBy(x => x.Id).AsQueryable();
if (skip.HasValue)
{
query = query.Skip(skip.Value);
}
if (take.HasValue)
{
query = query.Take(take.Value);
}
return await query.ToListAsync(cancellationToken);
}
public async Task<Massdata> UpsertByCustomerNameAsync(string customerName, decimal amount, bool statusFlag, string category, CancellationToken cancellationToken = default)
{
var customerParam = new SqlParameter("@CustomerName", SqlDbType.VarChar, 200) { Value = customerName };
var amountParam = new SqlParameter("@Amount", SqlDbType.Decimal) { Value = amount, Precision = 12, Scale = 2 };
var statusParam = new SqlParameter("@StatusFlag", SqlDbType.Bit) { Value = statusFlag };
var categoryParam = new SqlParameter("@Category", SqlDbType.VarChar, 100) { Value = category };
await _db.Database.ExecuteSqlRawAsync(
"EXEC dbo.PRMassdata_UpsertByCustomerName @CustomerName, @Amount, @StatusFlag, @Category",
parameters: new[] { customerParam, amountParam, statusParam, categoryParam },
cancellationToken: cancellationToken);
var updated = await GetByCustomerNameAsync(customerName, cancellationToken);
if (updated == null)
{
throw new InvalidOperationException("Upsert completed but record could not be loaded.");
}
return updated;
}
}

View File

@@ -11,8 +11,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DbFirst.Infrastructure", "D
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DbFirst.Domain", "DbFirst.Domain\DbFirst.Domain.csproj", "{E989468B-CBF1-49F4-954E-4FFEE7CE5A77}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DbFirst.Domain", "DbFirst.Domain\DbFirst.Domain.csproj", "{E989468B-CBF1-49F4-954E-4FFEE7CE5A77}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DbFirst.BlazorWasm", "DbFirst.BlazorWasm\DbFirst.BlazorWasm.csproj", "{666BE786-6D04-4224-9948-FF13597481A0}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DbFirst.BlazorWebApp", "DbFirst.BlazorWebApp\DbFirst.BlazorWebApp.csproj", "{FF1D77C4-A13D-43E0-BCE1-C18C01F1767C}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DbFirst.BlazorWebApp", "DbFirst.BlazorWebApp\DbFirst.BlazorWebApp.csproj", "{FF1D77C4-A13D-43E0-BCE1-C18C01F1767C}"
EndProject EndProject
Global Global
@@ -73,18 +71,6 @@ Global
{E989468B-CBF1-49F4-954E-4FFEE7CE5A77}.Release|x64.Build.0 = Release|Any CPU {E989468B-CBF1-49F4-954E-4FFEE7CE5A77}.Release|x64.Build.0 = Release|Any CPU
{E989468B-CBF1-49F4-954E-4FFEE7CE5A77}.Release|x86.ActiveCfg = Release|Any CPU {E989468B-CBF1-49F4-954E-4FFEE7CE5A77}.Release|x86.ActiveCfg = Release|Any CPU
{E989468B-CBF1-49F4-954E-4FFEE7CE5A77}.Release|x86.Build.0 = Release|Any CPU {E989468B-CBF1-49F4-954E-4FFEE7CE5A77}.Release|x86.Build.0 = Release|Any CPU
{666BE786-6D04-4224-9948-FF13597481A0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{666BE786-6D04-4224-9948-FF13597481A0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{666BE786-6D04-4224-9948-FF13597481A0}.Debug|x64.ActiveCfg = Debug|Any CPU
{666BE786-6D04-4224-9948-FF13597481A0}.Debug|x64.Build.0 = Debug|Any CPU
{666BE786-6D04-4224-9948-FF13597481A0}.Debug|x86.ActiveCfg = Debug|Any CPU
{666BE786-6D04-4224-9948-FF13597481A0}.Debug|x86.Build.0 = Debug|Any CPU
{666BE786-6D04-4224-9948-FF13597481A0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{666BE786-6D04-4224-9948-FF13597481A0}.Release|Any CPU.Build.0 = Release|Any CPU
{666BE786-6D04-4224-9948-FF13597481A0}.Release|x64.ActiveCfg = Release|Any CPU
{666BE786-6D04-4224-9948-FF13597481A0}.Release|x64.Build.0 = Release|Any CPU
{666BE786-6D04-4224-9948-FF13597481A0}.Release|x86.ActiveCfg = Release|Any CPU
{666BE786-6D04-4224-9948-FF13597481A0}.Release|x86.Build.0 = Release|Any CPU
{FF1D77C4-A13D-43E0-BCE1-C18C01F1767C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {FF1D77C4-A13D-43E0-BCE1-C18C01F1767C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FF1D77C4-A13D-43E0-BCE1-C18C01F1767C}.Debug|Any CPU.Build.0 = Debug|Any CPU {FF1D77C4-A13D-43E0-BCE1-C18C01F1767C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FF1D77C4-A13D-43E0-BCE1-C18C01F1767C}.Debug|x64.ActiveCfg = Debug|Any CPU {FF1D77C4-A13D-43E0-BCE1-C18C01F1767C}.Debug|x64.ActiveCfg = Debug|Any CPU