Compare commits
40 Commits
910b0e4aaa
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dc2cccac1f | ||
|
|
32b6d30ba1 | ||
|
|
940df826f7 | ||
|
|
7ca37dbfca | ||
|
|
9db55fd2fd | ||
|
|
70e5cbc19f | ||
|
|
1667788558 | ||
|
|
b09ee6dc8d | ||
|
|
0213834858 | ||
|
|
38baf9f749 | ||
|
|
0532cbb329 | ||
|
|
f9a6341b41 | ||
|
|
98b841196e | ||
|
|
05964eb02e | ||
|
|
e55f215210 | ||
|
|
6b89f7bd72 | ||
|
|
7a78a48d03 | ||
|
|
0b3249cb46 | ||
|
|
17fdb6ed51 | ||
|
|
166acea8b1 | ||
|
|
6c2b1884d2 | ||
|
|
3653def773 | ||
|
|
8d3783cfec | ||
|
|
0af0c4589d | ||
|
|
26f783e835 | ||
| d608ab1a6d | |||
|
|
4fbcd0dc11 | ||
|
|
45e5327148 | ||
|
|
9387db9824 | ||
|
|
ef76599bce | ||
|
|
870b10779e | ||
|
|
c8c75b1dc5 | ||
|
|
8c31784a5a | ||
|
|
28bab05980 | ||
|
|
289dba9b16 | ||
|
|
353611d400 | ||
|
|
8c175de953 | ||
|
|
1fd776bc29 | ||
|
|
904e6e20f0 | ||
|
|
215e526230 |
@@ -1,4 +1,8 @@
|
||||
using DbFirst.Application.Catalogs;
|
||||
using DbFirst.Application.Catalogs.Commands;
|
||||
using DbFirst.Application.Catalogs.Queries;
|
||||
using DbFirst.Domain;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace DbFirst.API.Controllers;
|
||||
@@ -7,24 +11,24 @@ namespace DbFirst.API.Controllers;
|
||||
[Route("api/[controller]")]
|
||||
public class CatalogsController : ControllerBase
|
||||
{
|
||||
private readonly ICatalogService _service;
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
public CatalogsController(ICatalogService service)
|
||||
public CatalogsController(IMediator mediator)
|
||||
{
|
||||
_service = service;
|
||||
_mediator = mediator;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<IEnumerable<CatalogReadDto>>> GetAll(CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await _service.GetAllAsync(cancellationToken);
|
||||
var result = await _mediator.Send(new GetAllCatalogsQuery(), cancellationToken);
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
[HttpGet("{id:int}")]
|
||||
public async Task<ActionResult<CatalogReadDto>> GetById(int id, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await _service.GetByIdAsync(id, cancellationToken);
|
||||
var result = await _mediator.Send(new GetCatalogByIdQuery(id), cancellationToken);
|
||||
if (result == null)
|
||||
{
|
||||
return NotFound();
|
||||
@@ -35,14 +39,29 @@ public class CatalogsController : ControllerBase
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<CatalogReadDto>> Create(CatalogWriteDto dto, CancellationToken cancellationToken)
|
||||
{
|
||||
var created = await _service.CreateAsync(dto, cancellationToken);
|
||||
var created = await _mediator.Send(new CreateCatalogCommand(dto), cancellationToken);
|
||||
if (created == null)
|
||||
{
|
||||
return Conflict();
|
||||
}
|
||||
return CreatedAtAction(nameof(GetById), new { id = created.Guid }, created);
|
||||
}
|
||||
|
||||
[HttpPut("{id:int}")]
|
||||
public async Task<ActionResult<CatalogReadDto>> Update(int id, CatalogWriteDto dto, CancellationToken cancellationToken)
|
||||
{
|
||||
var updated = await _service.UpdateAsync(id, dto, cancellationToken);
|
||||
var current = await _mediator.Send(new GetCatalogByIdQuery(id), cancellationToken);
|
||||
if (current == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
if (dto.UpdateProcedure == CatalogUpdateProcedure.Update &&
|
||||
!string.Equals(current.CatTitle, dto.CatTitle, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return BadRequest("Titel kann nicht geändert werden.");
|
||||
}
|
||||
|
||||
var updated = await _mediator.Send(new UpdateCatalogCommand(id, dto), cancellationToken);
|
||||
if (updated == null)
|
||||
{
|
||||
return NotFound();
|
||||
@@ -53,7 +72,7 @@ public class CatalogsController : ControllerBase
|
||||
[HttpDelete("{id:int}")]
|
||||
public async Task<IActionResult> Delete(int id, CancellationToken cancellationToken)
|
||||
{
|
||||
var deleted = await _service.DeleteAsync(id, cancellationToken);
|
||||
var deleted = await _mediator.Send(new DeleteCatalogCommand(id), cancellationToken);
|
||||
if (!deleted)
|
||||
{
|
||||
return NotFound();
|
||||
|
||||
14
DbFirst.API/Controllers/DefaultDashboardController.cs
Normal file
14
DbFirst.API/Controllers/DefaultDashboardController.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using DevExpress.DashboardAspNetCore;
|
||||
using DevExpress.DashboardWeb;
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
|
||||
namespace BlazorDashboardApp.Server
|
||||
{
|
||||
public class DefaultDashboardController : DashboardController
|
||||
{
|
||||
public DefaultDashboardController(DashboardConfigurator configurator, IDataProtectionProvider? dataProtectionProvider = null)
|
||||
: base(configurator, dataProtectionProvider)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
132
DbFirst.API/Dashboards/SqlDashboardStorage.cs
Normal file
132
DbFirst.API/Dashboards/SqlDashboardStorage.cs
Normal file
@@ -0,0 +1,132 @@
|
||||
using System.Data;
|
||||
using System.Text;
|
||||
using System.Xml.Linq;
|
||||
using DevExpress.DashboardWeb;
|
||||
using Microsoft.Data.SqlClient;
|
||||
|
||||
namespace DbFirst.API.Dashboards;
|
||||
|
||||
public sealed class SqlDashboardStorage : IEditableDashboardStorage
|
||||
{
|
||||
private readonly string _connectionString;
|
||||
private readonly string _tableName;
|
||||
private readonly Func<string?>? _userProvider;
|
||||
|
||||
public SqlDashboardStorage(string connectionString, string tableName = "TBDD_SMF_CONFIG", Func<string?>? userProvider = null)
|
||||
{
|
||||
_connectionString = connectionString;
|
||||
_tableName = tableName;
|
||||
_userProvider = userProvider;
|
||||
}
|
||||
|
||||
public IEnumerable<DashboardInfo> GetAvailableDashboardsInfo()
|
||||
{
|
||||
var dashboards = new List<DashboardInfo>();
|
||||
|
||||
using var connection = new SqlConnection(_connectionString);
|
||||
using var command = new SqlCommand($"SELECT DashboardId, DashboardName FROM dbo.[{_tableName}] WHERE ACTIVE = 1 ORDER BY DashboardName", connection);
|
||||
|
||||
connection.Open();
|
||||
using var reader = command.ExecuteReader();
|
||||
while (reader.Read())
|
||||
{
|
||||
var id = reader.GetString(0);
|
||||
var name = reader.GetString(1);
|
||||
dashboards.Add(new DashboardInfo { ID = id, Name = name });
|
||||
}
|
||||
|
||||
return dashboards;
|
||||
}
|
||||
|
||||
public XDocument LoadDashboard(string dashboardId)
|
||||
{
|
||||
using var connection = new SqlConnection(_connectionString);
|
||||
using var command = new SqlCommand($"SELECT DashboardData FROM dbo.[{_tableName}] WHERE DashboardId = @Id AND ACTIVE = 1", connection);
|
||||
command.Parameters.Add(new SqlParameter("@Id", SqlDbType.NVarChar, 128) { Value = dashboardId });
|
||||
|
||||
connection.Open();
|
||||
var data = command.ExecuteScalar() as byte[];
|
||||
if (data == null)
|
||||
{
|
||||
throw new ArgumentException($"Dashboard '{dashboardId}' not found.");
|
||||
}
|
||||
|
||||
var xml = Encoding.UTF8.GetString(data);
|
||||
var doc = XDocument.Parse(xml);
|
||||
NormalizeCatalogDateDimensions(doc);
|
||||
return doc;
|
||||
}
|
||||
|
||||
private static void NormalizeCatalogDateDimensions(XDocument doc)
|
||||
{
|
||||
var dateMembers = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"AddedWhen",
|
||||
"ChangedWhen"
|
||||
};
|
||||
|
||||
foreach (var dimension in doc.Descendants("Dimension"))
|
||||
{
|
||||
var member = dimension.Attribute("DataMember")?.Value;
|
||||
if (member == null || !dateMembers.Contains(member))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var interval = dimension.Attribute("DateTimeGroupInterval")?.Value;
|
||||
if (string.IsNullOrWhiteSpace(interval) || string.Equals(interval, "Year", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
dimension.SetAttributeValue("DateTimeGroupInterval", "DayMonthYear");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string AddDashboard(XDocument dashboard, string dashboardName)
|
||||
{
|
||||
var id = string.IsNullOrWhiteSpace(dashboardName)
|
||||
? Guid.NewGuid().ToString("N")
|
||||
: dashboardName;
|
||||
var payload = Encoding.UTF8.GetBytes(dashboard.ToString(SaveOptions.DisableFormatting));
|
||||
var userName = _userProvider?.Invoke();
|
||||
|
||||
using var connection = new SqlConnection(_connectionString);
|
||||
using var command = new SqlCommand($"INSERT INTO dbo.[{_tableName}] (ACTIVE, DashboardId, DashboardName, DashboardData, ADDED_WHO, ADDED_WHEN) VALUES (1, @Id, @Name, @Data, COALESCE(@User, SUSER_SNAME()), SYSUTCDATETIME())", connection);
|
||||
command.Parameters.Add(new SqlParameter("@Id", SqlDbType.NVarChar, 128) { Value = id });
|
||||
command.Parameters.Add(new SqlParameter("@Name", SqlDbType.NVarChar, 256) { Value = string.IsNullOrWhiteSpace(dashboardName) ? id : dashboardName });
|
||||
command.Parameters.Add(new SqlParameter("@Data", SqlDbType.VarBinary, -1) { Value = payload });
|
||||
command.Parameters.Add(new SqlParameter("@User", SqlDbType.NVarChar, 50) { Value = (object?)userName ?? DBNull.Value });
|
||||
|
||||
connection.Open();
|
||||
command.ExecuteNonQuery();
|
||||
return id;
|
||||
}
|
||||
|
||||
public void SaveDashboard(string dashboardId, XDocument dashboard)
|
||||
{
|
||||
var payload = Encoding.UTF8.GetBytes(dashboard.ToString(SaveOptions.DisableFormatting));
|
||||
var userName = _userProvider?.Invoke();
|
||||
|
||||
using var connection = new SqlConnection(_connectionString);
|
||||
using var command = new SqlCommand($"UPDATE dbo.[{_tableName}] SET DashboardData = @Data, CHANGED_WHO = COALESCE(@User, SUSER_SNAME()), CHANGED_WHEN = SYSUTCDATETIME() WHERE DashboardId = @Id", connection);
|
||||
command.Parameters.Add(new SqlParameter("@Id", SqlDbType.NVarChar, 128) { Value = dashboardId });
|
||||
command.Parameters.Add(new SqlParameter("@Data", SqlDbType.VarBinary, -1) { Value = payload });
|
||||
command.Parameters.Add(new SqlParameter("@User", SqlDbType.NVarChar, 50) { Value = (object?)userName ?? DBNull.Value });
|
||||
|
||||
connection.Open();
|
||||
var rows = command.ExecuteNonQuery();
|
||||
if (rows == 0)
|
||||
{
|
||||
throw new ArgumentException($"Dashboard '{dashboardId}' not found.");
|
||||
}
|
||||
}
|
||||
|
||||
public void DeleteDashboard(string dashboardId)
|
||||
{
|
||||
using var connection = new SqlConnection(_connectionString);
|
||||
using var command = new SqlCommand($"DELETE FROM dbo.[{_tableName}] WHERE DashboardId = @Id", connection);
|
||||
command.Parameters.Add(new SqlParameter("@Id", SqlDbType.NVarChar, 128) { Value = dashboardId });
|
||||
|
||||
connection.Open();
|
||||
command.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
52
DbFirst.API/Data/Dashboards/CatalogsGrid.xml
Normal file
52
DbFirst.API/Data/Dashboards/CatalogsGrid.xml
Normal file
@@ -0,0 +1,52 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Dashboard CurrencyCulture="de-DE" RequestParameters="false">
|
||||
<Title Text="Catalogs (Dashboard Grid)" />
|
||||
<DataSources>
|
||||
<JsonDataSource Name="Catalogs (API)" ComponentName="catalogsDataSource">
|
||||
<Source SourceType="DevExpress.DataAccess.Json.UriJsonSource" Uri="https://localhost:7204/api/catalogs" />
|
||||
</JsonDataSource>
|
||||
</DataSources>
|
||||
<Items>
|
||||
<Grid ComponentName="gridDashboardItem1" Name="Catalogs" DataSource="catalogsDataSource">
|
||||
<DataItems>
|
||||
<Dimension DataMember="Guid" DefaultId="DataItem0" />
|
||||
<Dimension DataMember="CatTitle" DefaultId="DataItem1" />
|
||||
<Dimension DataMember="CatString" DefaultId="DataItem2" />
|
||||
<Dimension DataMember="AddedWho" DefaultId="DataItem3" />
|
||||
<Dimension DataMember="AddedWhen" DefaultId="DataItem4" />
|
||||
<Dimension DataMember="ChangedWho" DefaultId="DataItem5" />
|
||||
<Dimension DataMember="ChangedWhen" DefaultId="DataItem6" />
|
||||
</DataItems>
|
||||
<GridColumns>
|
||||
<GridDimensionColumn Name="Id">
|
||||
<Dimension DefaultId="DataItem0" />
|
||||
</GridDimensionColumn>
|
||||
<GridDimensionColumn Name="Titel">
|
||||
<Dimension DefaultId="DataItem1" />
|
||||
</GridDimensionColumn>
|
||||
<GridDimensionColumn Name="String">
|
||||
<Dimension DefaultId="DataItem2" />
|
||||
</GridDimensionColumn>
|
||||
<GridDimensionColumn Name="Angelegt von">
|
||||
<Dimension DefaultId="DataItem3" />
|
||||
</GridDimensionColumn>
|
||||
<GridDimensionColumn Name="Angelegt am">
|
||||
<Dimension DefaultId="DataItem4" />
|
||||
</GridDimensionColumn>
|
||||
<GridDimensionColumn Name="Geändert von">
|
||||
<Dimension DefaultId="DataItem5" />
|
||||
</GridDimensionColumn>
|
||||
<GridDimensionColumn Name="Geändert am">
|
||||
<Dimension DefaultId="DataItem6" />
|
||||
</GridDimensionColumn>
|
||||
</GridColumns>
|
||||
<GridOptions />
|
||||
<ColumnFilterOptions />
|
||||
</Grid>
|
||||
</Items>
|
||||
<LayoutTree>
|
||||
<LayoutGroup Orientation="Vertical">
|
||||
<LayoutItem DashboardItem="gridDashboardItem1" />
|
||||
</LayoutGroup>
|
||||
</LayoutTree>
|
||||
</Dashboard>
|
||||
17
DbFirst.API/Data/Dashboards/DashboardStorage.sql
Normal file
17
DbFirst.API/Data/Dashboards/DashboardStorage.sql
Normal file
@@ -0,0 +1,17 @@
|
||||
CREATE TABLE dbo.TBDD_SMF_CONFIG (
|
||||
[GUID] [bigint] IDENTITY(1,1) NOT NULL,
|
||||
[ACTIVE] [bit] NOT NULL,
|
||||
DashboardId NVARCHAR(128) NOT NULL,
|
||||
DashboardName NVARCHAR(256) NOT NULL,
|
||||
DashboardData VARBINARY(MAX) NOT NULL,
|
||||
--- INSERT YOUR COLUMNS HERE ---
|
||||
[ADDED_WHO] [nvarchar](50) NOT NULL,
|
||||
[ADDED_WHEN] [datetime] NOT NULL,
|
||||
[CHANGED_WHO] [nvarchar](50) NULL,
|
||||
[CHANGED_WHEN] [datetime] NULL,
|
||||
CONSTRAINT [PK_TBDD_SMF_CONFIG_DashboardStorage] PRIMARY KEY CLUSTERED
|
||||
(
|
||||
[GUID] ASC
|
||||
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = ON, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, FILLFACTOR = 80) ON [PRIMARY]
|
||||
) ON [PRIMARY]
|
||||
GO
|
||||
55
DbFirst.API/Data/Dashboards/DefaultDashboard.xml
Normal file
55
DbFirst.API/Data/Dashboards/DefaultDashboard.xml
Normal file
@@ -0,0 +1,55 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Dashboard CurrencyCulture="de-DE" RequestParameters="false">
|
||||
<Title Text="Default Dashboard" />
|
||||
<DataSources>
|
||||
<JsonDataSource Name="JSON Data Source (URL)" RootElement="Customers" ComponentName="jsonDataSource1">
|
||||
<Source SourceType="DevExpress.DataAccess.Json.UriJsonSource" Uri="https://raw.githubusercontent.com/DevExpress-Examples/DataSources/master/JSON/customers.json" />
|
||||
</JsonDataSource>
|
||||
<JsonDataSource Name="Catalogs (API)" ComponentName="catalogsDataSource">
|
||||
<Source SourceType="DevExpress.DataAccess.Json.UriJsonSource" Uri="https://localhost:7204/api/catalogs" />
|
||||
</JsonDataSource>
|
||||
</DataSources>
|
||||
<Items>
|
||||
<Grid ComponentName="gridDashboardItem2" Name="Grid 2" DataSource="catalogsDataSource">
|
||||
<DataItems>
|
||||
<Measure DataMember="guid" DefaultId="DataItem0" />
|
||||
<Dimension DataMember="catTitle" DefaultId="DataItem1" />
|
||||
<Dimension DataMember="catString" DefaultId="DataItem2" />
|
||||
<Dimension DataMember="addedWhen" DefaultId="DataItem3" />
|
||||
<Dimension DataMember="addedWho" DefaultId="DataItem4" />
|
||||
<Dimension DataMember="changedWhen" DefaultId="DataItem5" />
|
||||
<Dimension DataMember="changedWho" DefaultId="DataItem6" />
|
||||
</DataItems>
|
||||
<GridColumns>
|
||||
<GridMeasureColumn>
|
||||
<Measure DefaultId="DataItem0" />
|
||||
</GridMeasureColumn>
|
||||
<GridDimensionColumn>
|
||||
<Dimension DefaultId="DataItem1" />
|
||||
</GridDimensionColumn>
|
||||
<GridDimensionColumn>
|
||||
<Dimension DefaultId="DataItem2" />
|
||||
</GridDimensionColumn>
|
||||
<GridDimensionColumn>
|
||||
<Dimension DefaultId="DataItem3" />
|
||||
</GridDimensionColumn>
|
||||
<GridDimensionColumn>
|
||||
<Dimension DefaultId="DataItem4" />
|
||||
</GridDimensionColumn>
|
||||
<GridDimensionColumn>
|
||||
<Dimension DefaultId="DataItem5" />
|
||||
</GridDimensionColumn>
|
||||
<GridDimensionColumn>
|
||||
<Dimension DefaultId="DataItem6" />
|
||||
</GridDimensionColumn>
|
||||
</GridColumns>
|
||||
<GridOptions />
|
||||
<ColumnFilterOptions />
|
||||
</Grid>
|
||||
</Items>
|
||||
<LayoutTree>
|
||||
<LayoutGroup>
|
||||
<LayoutItem DashboardItem="gridDashboardItem2" />
|
||||
</LayoutGroup>
|
||||
</LayoutTree>
|
||||
</Dashboard>
|
||||
@@ -9,6 +9,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AutoMapper" Version="12.0.1" />
|
||||
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1" />
|
||||
<PackageReference Include="DevExpress.AspNetCore.Dashboard" Version="25.2.3" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.22" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.22" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.22">
|
||||
@@ -16,6 +17,7 @@
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
|
||||
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="11.1.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
52
DbFirst.API/Middleware/ExceptionHandlingMiddleware.cs
Normal file
52
DbFirst.API/Middleware/ExceptionHandlingMiddleware.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
using System.Net;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace DbFirst.API.Middleware;
|
||||
|
||||
public class ExceptionHandlingMiddleware
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
private readonly ILogger<ExceptionHandlingMiddleware> _logger;
|
||||
|
||||
public ExceptionHandlingMiddleware(RequestDelegate next, ILogger<ExceptionHandlingMiddleware> logger)
|
||||
{
|
||||
_next = next;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task InvokeAsync(HttpContext context)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _next(context);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Unhandled exception");
|
||||
await WriteProblemDetailsAsync(context, ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task WriteProblemDetailsAsync(HttpContext context, Exception ex)
|
||||
{
|
||||
if (context.Response.HasStarted)
|
||||
{
|
||||
throw ex;
|
||||
}
|
||||
|
||||
context.Response.Clear();
|
||||
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
|
||||
context.Response.ContentType = "application/json";
|
||||
|
||||
var problem = new
|
||||
{
|
||||
type = "https://tools.ietf.org/html/rfc9110#section-15.6.1",
|
||||
title = "Serverfehler",
|
||||
status = context.Response.StatusCode,
|
||||
detail = ex.Message,
|
||||
traceId = context.TraceIdentifier
|
||||
};
|
||||
|
||||
await context.Response.WriteAsync(JsonSerializer.Serialize(problem));
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,17 @@
|
||||
using DbFirst.Application.Catalogs;
|
||||
using DbFirst.Domain.Repositories;
|
||||
using DbFirst.API.Middleware;
|
||||
using DbFirst.API.Dashboards;
|
||||
using DbFirst.Application;
|
||||
using DbFirst.Application.Repositories;
|
||||
using DbFirst.Domain;
|
||||
using DbFirst.Domain.Entities;
|
||||
using DbFirst.Infrastructure;
|
||||
using DbFirst.Infrastructure.Repositories;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
//TODO: create and add exception handling middleware
|
||||
using DevExpress.AspNetCore;
|
||||
using DevExpress.DashboardAspNetCore;
|
||||
using DevExpress.DashboardCommon;
|
||||
using DevExpress.DashboardWeb;
|
||||
using DevExpress.DataAccess.Json;
|
||||
using System.Xml.Linq;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
@@ -14,25 +21,122 @@ builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddSwaggerGen();
|
||||
|
||||
// TODO: allow listed origins configured in appsettings.json
|
||||
// In any case, dont let them to free to use without cors. if there is no origin specified, block all.
|
||||
// In development you can keep it easy.
|
||||
builder.Services.AddCors(options =>
|
||||
{
|
||||
options.AddDefaultPolicy(policy =>
|
||||
{
|
||||
policy.AllowAnyOrigin()
|
||||
.AllowAnyHeader()
|
||||
.AllowAnyMethod();
|
||||
if (builder.Environment.IsDevelopment())
|
||||
{
|
||||
policy.AllowAnyOrigin()
|
||||
.AllowAnyHeader()
|
||||
.AllowAnyMethod();
|
||||
}
|
||||
else
|
||||
{
|
||||
var origins = builder.Configuration.GetSection("Cors:AllowedOrigins").Get<string[]>() ?? Array.Empty<string>();
|
||||
if (origins.Length > 0)
|
||||
{
|
||||
policy.WithOrigins(origins)
|
||||
.AllowAnyHeader()
|
||||
.AllowAnyMethod();
|
||||
}
|
||||
// if no origins configured, deny all by leaving policy without allowances
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: Create extension method for this in Infrastructure layer in case of using in multiple projects
|
||||
builder.Services.AddDbContext<ApplicationDbContext>(options =>
|
||||
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
|
||||
|
||||
// TODO: Create extension method for this in Application layer in case of using in multiple projects
|
||||
builder.Services.AddAutoMapper(typeof(CatalogProfile).Assembly, typeof(ApplicationDbContext).Assembly);
|
||||
builder.Services.AddInfrastructure(builder.Configuration);
|
||||
builder.Services.AddApplication();
|
||||
|
||||
builder.Services.AddScoped<ICatalogRepository, CatalogRepository>();
|
||||
builder.Services.AddScoped<ICatalogService, CatalogService>();
|
||||
|
||||
builder.Services.AddDevExpressControls();
|
||||
builder.Services.AddScoped<DashboardConfigurator>((IServiceProvider serviceProvider) => {
|
||||
var dashboardsPath = Path.Combine(builder.Environment.ContentRootPath, "Data", "Dashboards");
|
||||
Directory.CreateDirectory(dashboardsPath);
|
||||
|
||||
var defaultDashboardPath = Path.Combine(dashboardsPath, "DefaultDashboard.xml");
|
||||
if (!File.Exists(defaultDashboardPath))
|
||||
{
|
||||
var defaultDashboard = new Dashboard();
|
||||
defaultDashboard.Title.Text = "Default Dashboard";
|
||||
defaultDashboard.SaveToXml(defaultDashboardPath);
|
||||
}
|
||||
|
||||
var dashboardBaseUrl = builder.Configuration["Dashboard:BaseUrl"]
|
||||
?? builder.Configuration["ApiBaseUrl"]
|
||||
?? builder.Configuration["ASPNETCORE_URLS"]?.Split(';', StringSplitOptions.RemoveEmptyEntries).FirstOrDefault()
|
||||
?? "https://localhost:7204";
|
||||
|
||||
dashboardBaseUrl = dashboardBaseUrl.TrimEnd('/');
|
||||
|
||||
var catalogsGridDashboardPath = Path.Combine(dashboardsPath, "CatalogsGrid.xml");
|
||||
if (!File.Exists(catalogsGridDashboardPath))
|
||||
{
|
||||
var dashboard = new Dashboard();
|
||||
dashboard.Title.Text = "Catalogs (Dashboard Grid)";
|
||||
|
||||
var catalogDataSource = new DashboardJsonDataSource("Catalogs (API)")
|
||||
{
|
||||
ComponentName = "catalogsDataSource",
|
||||
JsonSource = new UriJsonSource(new Uri($"{dashboardBaseUrl}/api/catalogs"))
|
||||
};
|
||||
|
||||
dashboard.DataSources.Add(catalogDataSource);
|
||||
|
||||
var grid = new GridDashboardItem
|
||||
{
|
||||
DataSource = catalogDataSource,
|
||||
Name = "Catalogs"
|
||||
};
|
||||
|
||||
grid.Columns.Add(new GridDimensionColumn(new Dimension(nameof(VwmyCatalog.Guid))) { Name = "Id" });
|
||||
grid.Columns.Add(new GridDimensionColumn(new Dimension(nameof(VwmyCatalog.CatTitle))) { Name = "Titel" });
|
||||
grid.Columns.Add(new GridDimensionColumn(new Dimension(nameof(VwmyCatalog.CatString))) { Name = "String" });
|
||||
grid.Columns.Add(new GridDimensionColumn(new Dimension(nameof(VwmyCatalog.AddedWho))) { Name = "Angelegt von" });
|
||||
grid.Columns.Add(new GridDimensionColumn(new Dimension(nameof(VwmyCatalog.AddedWhen))) { Name = "Angelegt am" });
|
||||
grid.Columns.Add(new GridDimensionColumn(new Dimension(nameof(VwmyCatalog.ChangedWho))) { Name = "Geändert von" });
|
||||
grid.Columns.Add(new GridDimensionColumn(new Dimension(nameof(VwmyCatalog.ChangedWhen))) { Name = "Geändert am" });
|
||||
|
||||
dashboard.Items.Add(grid);
|
||||
|
||||
var layoutGroup = new DashboardLayoutGroup { Orientation = DashboardLayoutGroupOrientation.Vertical };
|
||||
layoutGroup.ChildNodes.Add(new DashboardLayoutItem(grid));
|
||||
dashboard.LayoutRoot = layoutGroup;
|
||||
|
||||
dashboard.SaveToXml(catalogsGridDashboardPath);
|
||||
}
|
||||
|
||||
DashboardConfigurator configurator = new DashboardConfigurator();
|
||||
|
||||
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") ?? string.Empty;
|
||||
var dashboardStorage = new SqlDashboardStorage(connectionString, "TBDD_SMF_CONFIG");
|
||||
configurator.SetDashboardStorage(dashboardStorage);
|
||||
|
||||
DataSourceInMemoryStorage dataSourceStorage = new DataSourceInMemoryStorage();
|
||||
DashboardJsonDataSource jsonDataSourceUrl = new DashboardJsonDataSource("JSON Data Source (URL)");
|
||||
jsonDataSourceUrl.JsonSource = new UriJsonSource(
|
||||
new Uri("https://raw.githubusercontent.com/DevExpress-Examples/DataSources/master/JSON/customers.json"));
|
||||
jsonDataSourceUrl.RootElement = "Customers";
|
||||
dataSourceStorage.RegisterDataSource("jsonDataSourceUrl", jsonDataSourceUrl.SaveToXml());
|
||||
|
||||
var catalogsJsonDataSource = new DashboardJsonDataSource("Catalogs (API)")
|
||||
{
|
||||
ComponentName = "catalogsDataSource",
|
||||
JsonSource = new UriJsonSource(new Uri($"{dashboardBaseUrl}/api/catalogs"))
|
||||
};
|
||||
dataSourceStorage.RegisterDataSource(catalogsJsonDataSource.ComponentName, catalogsJsonDataSource.SaveToXml());
|
||||
dataSourceStorage.RegisterDataSource(catalogsJsonDataSource.Name, catalogsJsonDataSource.SaveToXml());
|
||||
|
||||
configurator.SetDataSourceStorage(dataSourceStorage);
|
||||
|
||||
EnsureDashboardInStorage(dashboardStorage, "DefaultDashboard", defaultDashboardPath);
|
||||
EnsureDashboardInStorage(dashboardStorage, "CatalogsGrid", catalogsGridDashboardPath);
|
||||
|
||||
return configurator;
|
||||
});
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
@@ -43,10 +147,27 @@ if (app.Environment.IsDevelopment())
|
||||
app.UseSwaggerUI();
|
||||
}
|
||||
|
||||
app.UseMiddleware<ExceptionHandlingMiddleware>();
|
||||
|
||||
app.UseDevExpressControls();
|
||||
app.UseHttpsRedirection();
|
||||
app.UseCors();
|
||||
app.UseAuthorization();
|
||||
|
||||
app.MapDashboardRoute("api/dashboard", "DefaultDashboard");
|
||||
|
||||
app.MapControllers();
|
||||
|
||||
app.Run();
|
||||
|
||||
static void EnsureDashboardInStorage(IEditableDashboardStorage storage, string id, string filePath)
|
||||
{
|
||||
var exists = storage.GetAvailableDashboardsInfo().Any(info => string.Equals(info.ID, id, StringComparison.OrdinalIgnoreCase));
|
||||
if (exists || !File.Exists(filePath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var doc = XDocument.Load(filePath);
|
||||
storage.AddDashboard(doc, id);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,27 @@
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": "Server=SDD-VMP04-SQL17\\DD_DEVELOP01;Database=DD_ECM;User Id=sa;Password=dd;TrustServerCertificate=True;"
|
||||
},
|
||||
"Dashboard": {
|
||||
"BaseUrl": "https://localhost:7204"
|
||||
},
|
||||
"Cors": {
|
||||
"AllowedOrigins": [
|
||||
"https://localhost:7276",
|
||||
"http://localhost:5101"
|
||||
]
|
||||
},
|
||||
"TableConfigurations": {
|
||||
"VwmyCatalog": {
|
||||
"ViewName": "VWMY_CATALOG",
|
||||
"GuidColumnName": "GUID",
|
||||
"CatTitleColumnName": "CAT_TITLE",
|
||||
"CatStringColumnName": "CAT_STRING",
|
||||
"AddedWhoColumnName": "ADDED_WHO",
|
||||
"AddedWhenColumnName": "ADDED_WHEN",
|
||||
"ChangedWhoColumnName": "CHANGED_WHO",
|
||||
"ChangedWhenColumnName": "CHANGED_WHEN"
|
||||
}
|
||||
},
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
using AutoMapper;
|
||||
using DbFirst.Domain.Repositories;
|
||||
using DbFirst.Domain.Entities;
|
||||
|
||||
namespace DbFirst.Application.Catalogs;
|
||||
|
||||
//TODO: create generic service to reduce code duplication
|
||||
//TODO: implement CQRS pattern with MediatR
|
||||
public class CatalogService : ICatalogService
|
||||
{
|
||||
private readonly ICatalogRepository _repository;
|
||||
private readonly IMapper _mapper;
|
||||
|
||||
public CatalogService(ICatalogRepository repository, IMapper mapper)
|
||||
{
|
||||
_repository = repository;
|
||||
_mapper = mapper;
|
||||
}
|
||||
|
||||
public async Task<List<CatalogReadDto>> GetAllAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var items = await _repository.GetAllAsync(cancellationToken);
|
||||
return _mapper.Map<List<CatalogReadDto>>(items);
|
||||
}
|
||||
|
||||
public async Task<CatalogReadDto?> GetByIdAsync(int id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var item = await _repository.GetByIdAsync(id, cancellationToken);
|
||||
return item == null ? null : _mapper.Map<CatalogReadDto>(item);
|
||||
}
|
||||
|
||||
public async Task<CatalogReadDto> CreateAsync(CatalogWriteDto dto, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var entity = _mapper.Map<VwmyCatalog>(dto);
|
||||
entity.AddedWho = "system";
|
||||
entity.AddedWhen = DateTime.UtcNow;
|
||||
entity.ChangedWho = "system";
|
||||
entity.ChangedWhen = DateTime.UtcNow;
|
||||
|
||||
var created = await _repository.InsertAsync(entity, cancellationToken);
|
||||
return _mapper.Map<CatalogReadDto>(created);
|
||||
}
|
||||
|
||||
public async Task<CatalogReadDto?> UpdateAsync(int id, CatalogWriteDto dto, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var existing = await _repository.GetByIdAsync(id, cancellationToken);
|
||||
if (existing == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var entity = _mapper.Map<VwmyCatalog>(dto);
|
||||
entity.Guid = id;
|
||||
entity.AddedWho = existing.AddedWho;
|
||||
entity.AddedWhen = existing.AddedWhen;
|
||||
entity.ChangedWho = "system";
|
||||
entity.ChangedWhen = DateTime.UtcNow;
|
||||
|
||||
var updated = await _repository.UpdateAsync(id, entity, cancellationToken);
|
||||
return updated == null ? null : _mapper.Map<CatalogReadDto>(updated);
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteAsync(int id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _repository.DeleteAsync(id, cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
using DbFirst.Domain;
|
||||
|
||||
namespace DbFirst.Application.Catalogs;
|
||||
|
||||
public class CatalogWriteDto
|
||||
{
|
||||
public string CatTitle { get; set; } = null!;
|
||||
public string CatString { get; set; } = null!;
|
||||
public CatalogUpdateProcedure UpdateProcedure { get; set; } = CatalogUpdateProcedure.Update;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
using MediatR;
|
||||
|
||||
namespace DbFirst.Application.Catalogs.Commands;
|
||||
|
||||
public record CreateCatalogCommand(CatalogWriteDto Dto) : IRequest<CatalogReadDto?>;
|
||||
@@ -0,0 +1,36 @@
|
||||
using AutoMapper;
|
||||
using DbFirst.Application.Repositories;
|
||||
using DbFirst.Domain.Entities;
|
||||
using MediatR;
|
||||
|
||||
namespace DbFirst.Application.Catalogs.Commands;
|
||||
|
||||
public class CreateCatalogHandler : IRequestHandler<CreateCatalogCommand, CatalogReadDto?>
|
||||
{
|
||||
private readonly ICatalogRepository _repository;
|
||||
private readonly IMapper _mapper;
|
||||
|
||||
public CreateCatalogHandler(ICatalogRepository repository, IMapper mapper)
|
||||
{
|
||||
_repository = repository;
|
||||
_mapper = mapper;
|
||||
}
|
||||
|
||||
public async Task<CatalogReadDto?> Handle(CreateCatalogCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var existing = await _repository.GetByTitleAsync(request.Dto.CatTitle, cancellationToken);
|
||||
if (existing != null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var entity = _mapper.Map<VwmyCatalog>(request.Dto);
|
||||
entity.AddedWho = "system";
|
||||
entity.AddedWhen = DateTime.UtcNow;
|
||||
entity.ChangedWho = "system";
|
||||
entity.ChangedWhen = DateTime.UtcNow;
|
||||
|
||||
var created = await _repository.InsertAsync(entity, cancellationToken);
|
||||
return _mapper.Map<CatalogReadDto>(created);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
using MediatR;
|
||||
|
||||
namespace DbFirst.Application.Catalogs.Commands;
|
||||
|
||||
public record DeleteCatalogCommand(int Id) : IRequest<bool>;
|
||||
@@ -0,0 +1,19 @@
|
||||
using DbFirst.Application.Repositories;
|
||||
using MediatR;
|
||||
|
||||
namespace DbFirst.Application.Catalogs.Commands;
|
||||
|
||||
public class DeleteCatalogHandler : IRequestHandler<DeleteCatalogCommand, bool>
|
||||
{
|
||||
private readonly ICatalogRepository _repository;
|
||||
|
||||
public DeleteCatalogHandler(ICatalogRepository repository)
|
||||
{
|
||||
_repository = repository;
|
||||
}
|
||||
|
||||
public async Task<bool> Handle(DeleteCatalogCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
return await _repository.DeleteAsync(request.Id, cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
using MediatR;
|
||||
|
||||
namespace DbFirst.Application.Catalogs.Commands;
|
||||
|
||||
public record UpdateCatalogCommand(int Id, CatalogWriteDto Dto) : IRequest<CatalogReadDto?>;
|
||||
@@ -0,0 +1,42 @@
|
||||
using AutoMapper;
|
||||
using DbFirst.Application.Repositories;
|
||||
using DbFirst.Domain.Entities;
|
||||
using DbFirst.Domain;
|
||||
using MediatR;
|
||||
|
||||
namespace DbFirst.Application.Catalogs.Commands;
|
||||
|
||||
public class UpdateCatalogHandler : IRequestHandler<UpdateCatalogCommand, CatalogReadDto?>
|
||||
{
|
||||
private readonly ICatalogRepository _repository;
|
||||
private readonly IMapper _mapper;
|
||||
|
||||
public UpdateCatalogHandler(ICatalogRepository repository, IMapper mapper)
|
||||
{
|
||||
_repository = repository;
|
||||
_mapper = mapper;
|
||||
}
|
||||
|
||||
public async Task<CatalogReadDto?> Handle(UpdateCatalogCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var existing = await _repository.GetByIdAsync(request.Id, cancellationToken);
|
||||
if (existing == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var entity = _mapper.Map<VwmyCatalog>(request.Dto);
|
||||
entity.Guid = request.Id;
|
||||
entity.CatTitle = request.Dto.UpdateProcedure == CatalogUpdateProcedure.Update
|
||||
? existing.CatTitle
|
||||
: request.Dto.CatTitle;
|
||||
entity.AddedWho = existing.AddedWho;
|
||||
entity.AddedWhen = existing.AddedWhen;
|
||||
entity.ChangedWho = "system";
|
||||
entity.ChangedWhen = DateTime.UtcNow;
|
||||
|
||||
var procedure = request.Dto.UpdateProcedure;
|
||||
var updated = await _repository.UpdateAsync(request.Id, entity, procedure, cancellationToken);
|
||||
return updated == null ? null : _mapper.Map<CatalogReadDto>(updated);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
namespace DbFirst.Application.Catalogs;
|
||||
|
||||
//TODO: create generic service to reduce code duplication
|
||||
public interface ICatalogService
|
||||
{
|
||||
Task<List<CatalogReadDto>> GetAllAsync(CancellationToken cancellationToken = default);
|
||||
Task<CatalogReadDto?> GetByIdAsync(int id, CancellationToken cancellationToken = default);
|
||||
Task<CatalogReadDto> CreateAsync(CatalogWriteDto dto, CancellationToken cancellationToken = default);
|
||||
Task<CatalogReadDto?> UpdateAsync(int id, CatalogWriteDto dto, CancellationToken cancellationToken = default);
|
||||
Task<bool> DeleteAsync(int id, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using AutoMapper;
|
||||
using DbFirst.Application.Repositories;
|
||||
using MediatR;
|
||||
|
||||
namespace DbFirst.Application.Catalogs.Queries;
|
||||
|
||||
public class GetAllCatalogsHandler : IRequestHandler<GetAllCatalogsQuery, List<CatalogReadDto>>
|
||||
{
|
||||
private readonly ICatalogRepository _repository;
|
||||
private readonly IMapper _mapper;
|
||||
|
||||
public GetAllCatalogsHandler(ICatalogRepository repository, IMapper mapper)
|
||||
{
|
||||
_repository = repository;
|
||||
_mapper = mapper;
|
||||
}
|
||||
|
||||
public async Task<List<CatalogReadDto>> Handle(GetAllCatalogsQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var items = await _repository.GetAllAsync(cancellationToken);
|
||||
return _mapper.Map<List<CatalogReadDto>>(items);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
using MediatR;
|
||||
|
||||
namespace DbFirst.Application.Catalogs.Queries;
|
||||
|
||||
public record GetAllCatalogsQuery : IRequest<List<CatalogReadDto>>;
|
||||
@@ -0,0 +1,23 @@
|
||||
using AutoMapper;
|
||||
using DbFirst.Application.Repositories;
|
||||
using MediatR;
|
||||
|
||||
namespace DbFirst.Application.Catalogs.Queries;
|
||||
|
||||
public class GetCatalogByIdHandler : IRequestHandler<GetCatalogByIdQuery, CatalogReadDto?>
|
||||
{
|
||||
private readonly ICatalogRepository _repository;
|
||||
private readonly IMapper _mapper;
|
||||
|
||||
public GetCatalogByIdHandler(ICatalogRepository repository, IMapper mapper)
|
||||
{
|
||||
_repository = repository;
|
||||
_mapper = mapper;
|
||||
}
|
||||
|
||||
public async Task<CatalogReadDto?> Handle(GetCatalogByIdQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var item = await _repository.GetByIdAsync(request.Id, cancellationToken);
|
||||
return item == null ? null : _mapper.Map<CatalogReadDto>(item);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
using MediatR;
|
||||
|
||||
namespace DbFirst.Application.Catalogs.Queries;
|
||||
|
||||
public record GetCatalogByIdQuery(int Id) : IRequest<CatalogReadDto?>;
|
||||
@@ -8,6 +8,9 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AutoMapper" Version="12.0.1" />
|
||||
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2" />
|
||||
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="11.1.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
14
DbFirst.Application/DependencyInjection.cs
Normal file
14
DbFirst.Application/DependencyInjection.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using MediatR;
|
||||
|
||||
namespace DbFirst.Application;
|
||||
|
||||
public static class DependencyInjection
|
||||
{
|
||||
public static IServiceCollection AddApplication(this IServiceCollection services)
|
||||
{
|
||||
services.AddAutoMapper(typeof(DependencyInjection).Assembly);
|
||||
services.AddMediatR(typeof(DependencyInjection).Assembly);
|
||||
return services;
|
||||
}
|
||||
}
|
||||
10
DbFirst.Application/Repositories/ICatalogRepository.cs
Normal file
10
DbFirst.Application/Repositories/ICatalogRepository.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using DbFirst.Domain;
|
||||
using DbFirst.Domain.Entities;
|
||||
|
||||
namespace DbFirst.Application.Repositories;
|
||||
|
||||
public interface ICatalogRepository : IRepository<VwmyCatalog>
|
||||
{
|
||||
Task<VwmyCatalog?> GetByTitleAsync(string title, CancellationToken cancellationToken = default);
|
||||
Task<VwmyCatalog?> UpdateAsync(int id, VwmyCatalog catalog, CatalogUpdateProcedure procedure, CancellationToken cancellationToken = default);
|
||||
}
|
||||
10
DbFirst.Application/Repositories/IRepository.cs
Normal file
10
DbFirst.Application/Repositories/IRepository.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace DbFirst.Application.Repositories;
|
||||
|
||||
public interface IRepository<T>
|
||||
{
|
||||
Task<List<T>> GetAllAsync(CancellationToken cancellationToken = default);
|
||||
Task<T?> GetByIdAsync(int id, CancellationToken cancellationToken = default);
|
||||
Task<T> InsertAsync(T entity, CancellationToken cancellationToken = default);
|
||||
Task<T?> UpdateAsync(int id, T entity, CancellationToken cancellationToken = default);
|
||||
Task<bool> DeleteAsync(int id, CancellationToken cancellationToken = default);
|
||||
}
|
||||
60
DbFirst.BlazorWasm/Ablauf.cs
Normal file
60
DbFirst.BlazorWasm/Ablauf.cs
Normal file
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
@@ -1,4 +1,12 @@
|
||||
<Router AppAssembly="@typeof(App).Assembly">
|
||||
@*
|
||||
• 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" />
|
||||
|
||||
296
DbFirst.BlazorWasm/Components/CatalogsGrid.razor
Normal file
296
DbFirst.BlazorWasm/Components/CatalogsGrid.razor
Normal file
@@ -0,0 +1,296 @@
|
||||
@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;
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,10 @@
|
||||
</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>
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
@inherits LayoutComponentBase
|
||||
@* Definiert das Hauptlayout der Anwendung.
|
||||
Enthält die Navigationsleiste und den Hauptinhalt. *@
|
||||
|
||||
@inherits LayoutComponentBase
|
||||
<div class="page">
|
||||
<div class="sidebar">
|
||||
<NavMenu />
|
||||
<NavMenu /> <!-- Einbindung der Navigationsleiste -->
|
||||
</div>
|
||||
|
||||
<main>
|
||||
@@ -10,7 +13,7 @@
|
||||
</div>
|
||||
|
||||
<article class="content px-4">
|
||||
@Body
|
||||
@Body <!-- Platzhalter für den Seiteninhalt -->
|
||||
</article>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<div class="top-row ps-3 navbar navbar-dark">
|
||||
@* 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">
|
||||
@@ -19,14 +21,21 @@
|
||||
<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;
|
||||
|
||||
@@ -4,4 +4,5 @@ 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
|
||||
}
|
||||
|
||||
7
DbFirst.BlazorWasm/Models/DashboardInfoDto.cs
Normal file
7
DbFirst.BlazorWasm/Models/DashboardInfoDto.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace DbFirst.BlazorWasm.Models;
|
||||
|
||||
public class DashboardInfoDto
|
||||
{
|
||||
public string Id { get; set; } = string.Empty;
|
||||
public string Name { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -1,212 +1,7 @@
|
||||
@page "/catalogs"
|
||||
@inject CatalogApiClient Api
|
||||
|
||||
<PageTitle>Catalogs</PageTitle>
|
||||
|
||||
<h1>Catalogs</h1>
|
||||
|
||||
@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">
|
||||
<button class="btn btn-primary" @onclick="StartCreate">Neuen Eintrag anlegen</button>
|
||||
</div>
|
||||
|
||||
@if (showForm)
|
||||
{
|
||||
<EditForm Model="formModel" OnValidSubmit="HandleSubmit">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Titel</label>
|
||||
<InputText class="form-control" @bind-Value="formModel.CatTitle" />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Kennung</label>
|
||||
<InputText class="form-control" @bind-Value="formModel.CatString" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 d-flex gap-2">
|
||||
<button type="submit" class="btn btn-success">@((isEditing ? "Speichern" : "Anlegen"))</button>
|
||||
<button type="button" class="btn btn-secondary" @onclick="CancelEdit">Abbrechen</button>
|
||||
</div>
|
||||
</EditForm>
|
||||
}
|
||||
|
||||
@if (isLoading)
|
||||
{
|
||||
<p><em>Lade Daten...</em></p>
|
||||
}
|
||||
else if (items.Count == 0)
|
||||
{
|
||||
<p>Keine Einträge vorhanden.</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Id</th>
|
||||
<th>Titel</th>
|
||||
<th>String</th>
|
||||
<th>Angelegt von</th>
|
||||
<th>Angelegt am</th>
|
||||
<th>Geändert von</th>
|
||||
<th>Geändert am</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var item in items)
|
||||
{
|
||||
<tr>
|
||||
<td>@item.Guid</td>
|
||||
<td>@item.CatTitle</td>
|
||||
<td>@item.CatString</td>
|
||||
<td>@item.AddedWho</td>
|
||||
<td>@item.AddedWhen.ToString("g")</td>
|
||||
<td>@item.ChangedWho</td>
|
||||
<td>@(item.ChangedWhen.HasValue ? item.ChangedWhen.Value.ToString("g") : string.Empty)</td>
|
||||
<td class="text-end">
|
||||
<button class="btn btn-sm btn-outline-primary me-2" @onclick="(() => StartEdit(item))">Bearbeiten</button>
|
||||
<button class="btn btn-sm btn-outline-danger" @onclick="(() => DeleteCatalog(item.Guid))">Löschen</button>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
|
||||
@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;
|
||||
|
||||
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
|
||||
};
|
||||
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)
|
||||
{
|
||||
errorMessage = "Aktualisierung fehlgeschlagen.";
|
||||
return;
|
||||
}
|
||||
|
||||
infoMessage = "Katalog aktualisiert.";
|
||||
}
|
||||
else
|
||||
{
|
||||
var created = await Api.CreateAsync(formModel);
|
||||
if (created == null)
|
||||
{
|
||||
errorMessage = "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)
|
||||
{
|
||||
errorMessage = "Löschen fehlgeschlagen.";
|
||||
return;
|
||||
}
|
||||
|
||||
infoMessage = "Katalog gelöscht.";
|
||||
await LoadCatalogs();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errorMessage = $"Fehler beim Löschen: {ex.Message}";
|
||||
}
|
||||
}
|
||||
}
|
||||
<CatalogsGrid />
|
||||
|
||||
122
DbFirst.BlazorWasm/Pages/Dashboard.razor
Normal file
122
DbFirst.BlazorWasm/Pages/Dashboard.razor
Normal file
@@ -0,0 +1,122 @@
|
||||
@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);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,6 @@
|
||||
|
||||
<PageTitle>Home</PageTitle>
|
||||
|
||||
<h1>Hello, world!</h1>
|
||||
<h1>Db First approach</h1>
|
||||
|
||||
Welcome to your new app.
|
||||
This is a Blazor WebAssembly application demonstrating the Database First approach using DevExpress components.
|
||||
|
||||
@@ -1,14 +1,22 @@
|
||||
/* 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();
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
/* 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;
|
||||
@@ -24,26 +29,120 @@ public class CatalogApiClient
|
||||
return await _httpClient.GetFromJsonAsync<CatalogReadDto>($"{Endpoint}/{id}");
|
||||
}
|
||||
|
||||
public async Task<CatalogReadDto?> CreateAsync(CatalogWriteDto dto)
|
||||
public async Task<ApiResult<CatalogReadDto?>> CreateAsync(CatalogWriteDto dto)
|
||||
{
|
||||
var response = await _httpClient.PostAsJsonAsync(Endpoint, dto);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
return null;
|
||||
var payload = await response.Content.ReadFromJsonAsync<CatalogReadDto>();
|
||||
return ApiResult<CatalogReadDto?>.Ok(payload);
|
||||
}
|
||||
|
||||
return await response.Content.ReadFromJsonAsync<CatalogReadDto>();
|
||||
var error = await ReadErrorAsync(response);
|
||||
return ApiResult<CatalogReadDto?>.Fail(error);
|
||||
}
|
||||
|
||||
public async Task<bool> UpdateAsync(int id, CatalogWriteDto dto)
|
||||
public async Task<ApiResult<bool>> UpdateAsync(int id, CatalogWriteDto dto)
|
||||
{
|
||||
var response = await _httpClient.PutAsJsonAsync($"{Endpoint}/{id}", dto);
|
||||
return response.IsSuccessStatusCode;
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
return ApiResult<bool>.Ok(true);
|
||||
}
|
||||
|
||||
var error = await ReadErrorAsync(response);
|
||||
return ApiResult<bool>.Fail(error);
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteAsync(int id)
|
||||
public async Task<ApiResult<bool>> DeleteAsync(int id)
|
||||
{
|
||||
var response = await _httpClient.DeleteAsync($"{Endpoint}/{id}");
|
||||
return response.IsSuccessStatusCode;
|
||||
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; }
|
||||
}
|
||||
|
||||
21
DbFirst.BlazorWasm/Services/DashboardApiClient.cs
Normal file
21
DbFirst.BlazorWasm/Services/DashboardApiClient.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
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>();
|
||||
}
|
||||
}
|
||||
@@ -10,3 +10,7 @@
|
||||
@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
|
||||
@@ -1,32 +1,58 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<!--
|
||||
• 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.
|
||||
-->
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>DbFirst.BlazorWasm</title>
|
||||
<base href="/" />
|
||||
<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>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<body>
|
||||
<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>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>DbFirst.BlazorWasm</title>
|
||||
<base href="/" />
|
||||
|
||||
<div id="blazor-error-ui">
|
||||
An unhandled error has occurred.
|
||||
<a href="" class="reload">Reload</a>
|
||||
<a class="dismiss">🗙</a>
|
||||
</div>
|
||||
<script src="_framework/blazor.webassembly.js"></script>
|
||||
</body>
|
||||
<!-- 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>
|
||||
|
||||
39
DbFirst.BlazorWebApp/Components/App.razor
Normal file
39
DbFirst.BlazorWebApp/Components/App.razor
Normal file
@@ -0,0 +1,39 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<base href="/" />
|
||||
@DxResourceManager.RegisterTheme(Themes.Fluent)
|
||||
@DxResourceManager.RegisterScripts()
|
||||
|
||||
<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="_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="app.css" />
|
||||
<link rel="stylesheet" href="DbFirst.BlazorWebApp.styles.css" />
|
||||
<link rel="icon" type="image/png" href="favicon.png" />
|
||||
<HeadOutlet />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<Routes />
|
||||
<script src="_framework/blazor.web.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
225
DbFirst.BlazorWebApp/Components/CatalogsGrid.razor
Normal file
225
DbFirst.BlazorWebApp/Components/CatalogsGrid.razor
Normal file
@@ -0,0 +1,225 @@
|
||||
@inject CatalogApiClient Api
|
||||
|
||||
<style>
|
||||
.action-panel { margin-bottom: 16px; }
|
||||
.grid-section { margin-top: 12px; }
|
||||
</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" />
|
||||
<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>
|
||||
</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;
|
||||
}
|
||||
}
|
||||
23
DbFirst.BlazorWebApp/Components/Layout/MainLayout.razor
Normal file
23
DbFirst.BlazorWebApp/Components/Layout/MainLayout.razor
Normal file
@@ -0,0 +1,23 @@
|
||||
@inherits LayoutComponentBase
|
||||
|
||||
<div class="page">
|
||||
<div class="sidebar">
|
||||
<NavMenu />
|
||||
</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
|
||||
</article>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<div id="blazor-error-ui">
|
||||
An unhandled error has occurred.
|
||||
<a href="" class="reload">Reload</a>
|
||||
<a class="dismiss">🗙</a>
|
||||
</div>
|
||||
96
DbFirst.BlazorWebApp/Components/Layout/MainLayout.razor.css
Normal file
96
DbFirst.BlazorWebApp/Components/Layout/MainLayout.razor.css
Normal file
@@ -0,0 +1,96 @@
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
#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;
|
||||
}
|
||||
41
DbFirst.BlazorWebApp/Components/Layout/NavMenu.razor
Normal file
41
DbFirst.BlazorWebApp/Components/Layout/NavMenu.razor
Normal file
@@ -0,0 +1,41 @@
|
||||
<div class="top-row ps-3 navbar navbar-dark">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="">DbFirst.BlazorWebApp</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="checkbox" title="Navigation menu" class="navbar-toggler" />
|
||||
|
||||
<div class="nav-scrollable" onclick="document.querySelector('.navbar-toggler').click()">
|
||||
<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="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">
|
||||
<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>
|
||||
|
||||
105
DbFirst.BlazorWebApp/Components/Layout/NavMenu.razor.css
Normal file
105
DbFirst.BlazorWebApp/Components/Layout/NavMenu.razor.css
Normal file
@@ -0,0 +1,105 @@
|
||||
.navbar-toggler {
|
||||
appearance: none;
|
||||
cursor: pointer;
|
||||
width: 3.5rem;
|
||||
height: 2.5rem;
|
||||
color: white;
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
right: 1rem;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e") no-repeat center/1.75rem rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.navbar-toggler:checked {
|
||||
background-color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.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 .nav-link {
|
||||
color: #d7d7d7;
|
||||
background: none;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
height: 3rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
line-height: 3rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.nav-item ::deep a.active {
|
||||
background-color: rgba(255,255,255,0.37);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nav-item ::deep .nav-link:hover {
|
||||
background-color: rgba(255,255,255,0.1);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nav-scrollable {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.navbar-toggler:checked ~ .nav-scrollable {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@media (min-width: 641px) {
|
||||
.navbar-toggler {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nav-scrollable {
|
||||
/* Never collapse the sidebar for wide screens */
|
||||
display: block;
|
||||
|
||||
/* Allow sidebar to scroll for tall menus */
|
||||
height: calc(100vh - 3.5rem);
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
7
DbFirst.BlazorWebApp/Components/Pages/Catalogs.razor
Normal file
7
DbFirst.BlazorWebApp/Components/Pages/Catalogs.razor
Normal file
@@ -0,0 +1,7 @@
|
||||
@page "/catalogs"
|
||||
|
||||
<PageTitle>Catalogs</PageTitle>
|
||||
|
||||
<h1>Catalogs</h1>
|
||||
|
||||
<CatalogsGrid />
|
||||
19
DbFirst.BlazorWebApp/Components/Pages/Counter.razor
Normal file
19
DbFirst.BlazorWebApp/Components/Pages/Counter.razor
Normal file
@@ -0,0 +1,19 @@
|
||||
@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++;
|
||||
}
|
||||
}
|
||||
122
DbFirst.BlazorWebApp/Components/Pages/Dashboard.razor
Normal file
122
DbFirst.BlazorWebApp/Components/Pages/Dashboard.razor
Normal file
@@ -0,0 +1,122 @@
|
||||
@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);
|
||||
}
|
||||
}
|
||||
36
DbFirst.BlazorWebApp/Components/Pages/Error.razor
Normal file
36
DbFirst.BlazorWebApp/Components/Pages/Error.razor
Normal file
@@ -0,0 +1,36 @@
|
||||
@page "/Error"
|
||||
@using System.Diagnostics
|
||||
|
||||
<PageTitle>Error</PageTitle>
|
||||
|
||||
<h1 class="text-danger">Error.</h1>
|
||||
<h2 class="text-danger">An error occurred while processing your request.</h2>
|
||||
|
||||
@if (ShowRequestId)
|
||||
{
|
||||
<p>
|
||||
<strong>Request ID:</strong> <code>@RequestId</code>
|
||||
</p>
|
||||
}
|
||||
|
||||
<h3>Development Mode</h3>
|
||||
<p>
|
||||
Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
|
||||
</p>
|
||||
<p>
|
||||
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
|
||||
It can result in displaying sensitive information from exceptions to end users.
|
||||
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
|
||||
and restarting the app.
|
||||
</p>
|
||||
|
||||
@code{
|
||||
[CascadingParameter]
|
||||
private HttpContext? HttpContext { get; set; }
|
||||
|
||||
private string? RequestId { get; set; }
|
||||
private bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
|
||||
|
||||
protected override void OnInitialized() =>
|
||||
RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier;
|
||||
}
|
||||
7
DbFirst.BlazorWebApp/Components/Pages/Home.razor
Normal file
7
DbFirst.BlazorWebApp/Components/Pages/Home.razor
Normal file
@@ -0,0 +1,7 @@
|
||||
@page "/"
|
||||
|
||||
<PageTitle>Home</PageTitle>
|
||||
|
||||
<h1>Hello, world!</h1>
|
||||
|
||||
Welcome to your new app.
|
||||
64
DbFirst.BlazorWebApp/Components/Pages/Weather.razor
Normal file
64
DbFirst.BlazorWebApp/Components/Pages/Weather.razor
Normal file
@@ -0,0 +1,64 @@
|
||||
@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);
|
||||
}
|
||||
}
|
||||
8
DbFirst.BlazorWebApp/Components/Routes.razor
Normal file
8
DbFirst.BlazorWebApp/Components/Routes.razor
Normal file
@@ -0,0 +1,8 @@
|
||||
@rendermode InteractiveServer
|
||||
|
||||
<Router AppAssembly="typeof(Program).Assembly">
|
||||
<Found Context="routeData">
|
||||
<RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
|
||||
<FocusOnNavigate RouteData="routeData" Selector="h1" />
|
||||
</Found>
|
||||
</Router>
|
||||
16
DbFirst.BlazorWebApp/Components/_Imports.razor
Normal file
16
DbFirst.BlazorWebApp/Components/_Imports.razor
Normal file
@@ -0,0 +1,16 @@
|
||||
@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 static Microsoft.AspNetCore.Components.Web.RenderMode
|
||||
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||
@using Microsoft.JSInterop
|
||||
@using DbFirst.BlazorWebApp
|
||||
@using DbFirst.BlazorWebApp.Components
|
||||
@using DbFirst.BlazorWebApp.Models
|
||||
@using DbFirst.BlazorWebApp.Services
|
||||
@using DevExpress.Blazor
|
||||
@using DevExpress.DashboardBlazor
|
||||
@using DevExpress.DashboardWeb
|
||||
@using DbFirst.BlazorWebApp
|
||||
16
DbFirst.BlazorWebApp/DbFirst.BlazorWebApp.csproj
Normal file
16
DbFirst.BlazorWebApp/DbFirst.BlazorWebApp.csproj
Normal file
@@ -0,0 +1,16 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<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" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
12
DbFirst.BlazorWebApp/Models/CatalogReadDto.cs
Normal file
12
DbFirst.BlazorWebApp/Models/CatalogReadDto.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace DbFirst.BlazorWebApp.Models;
|
||||
|
||||
public class CatalogReadDto
|
||||
{
|
||||
public int Guid { get; set; }
|
||||
public string CatTitle { get; set; } = null!;
|
||||
public string CatString { get; set; } = null!;
|
||||
public string AddedWho { get; set; } = null!;
|
||||
public DateTime AddedWhen { get; set; }
|
||||
public string? ChangedWho { get; set; }
|
||||
public DateTime? ChangedWhen { get; set; }
|
||||
}
|
||||
8
DbFirst.BlazorWebApp/Models/CatalogWriteDto.cs
Normal file
8
DbFirst.BlazorWebApp/Models/CatalogWriteDto.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace DbFirst.BlazorWebApp.Models;
|
||||
|
||||
public class CatalogWriteDto
|
||||
{
|
||||
public string CatTitle { get; set; } = string.Empty;
|
||||
public string CatString { get; set; } = string.Empty;
|
||||
public int UpdateProcedure { get; set; }
|
||||
}
|
||||
7
DbFirst.BlazorWebApp/Models/DashboardInfoDto.cs
Normal file
7
DbFirst.BlazorWebApp/Models/DashboardInfoDto.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace DbFirst.BlazorWebApp.Models;
|
||||
|
||||
public class DashboardInfoDto
|
||||
{
|
||||
public string Id { get; set; } = string.Empty;
|
||||
public string Name { get; set; } = string.Empty;
|
||||
}
|
||||
49
DbFirst.BlazorWebApp/Program.cs
Normal file
49
DbFirst.BlazorWebApp/Program.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
using DbFirst.BlazorWebApp.Components;
|
||||
using DbFirst.BlazorWebApp.Services;
|
||||
using DevExpress.Blazor;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Add services to the container.
|
||||
builder.Services.AddRazorComponents()
|
||||
.AddInteractiveServerComponents();
|
||||
|
||||
builder.Services.AddDevExpressBlazor();
|
||||
|
||||
var apiBaseUrl = builder.Configuration["ApiBaseUrl"];
|
||||
if (!string.IsNullOrWhiteSpace(apiBaseUrl))
|
||||
{
|
||||
builder.Services.AddHttpClient<CatalogApiClient>(client =>
|
||||
{
|
||||
client.BaseAddress = new Uri(apiBaseUrl);
|
||||
});
|
||||
builder.Services.AddHttpClient<DashboardApiClient>(client =>
|
||||
{
|
||||
client.BaseAddress = new Uri(apiBaseUrl);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.Services.AddHttpClient<CatalogApiClient>();
|
||||
builder.Services.AddHttpClient<DashboardApiClient>();
|
||||
}
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
if (!app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseExceptionHandler("/Error", createScopeForErrors: true);
|
||||
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
|
||||
app.UseHsts();
|
||||
}
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
|
||||
app.UseStaticFiles();
|
||||
app.UseAntiforgery();
|
||||
|
||||
app.MapRazorComponents<App>()
|
||||
.AddInteractiveServerRenderMode();
|
||||
|
||||
app.Run();
|
||||
38
DbFirst.BlazorWebApp/Properties/launchSettings.json
Normal file
38
DbFirst.BlazorWebApp/Properties/launchSettings.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"$schema": "http://json.schemastore.org/launchsettings.json",
|
||||
"iisSettings": {
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:7440",
|
||||
"sslPort": 44343
|
||||
}
|
||||
},
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "http://localhost:5096",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "https://localhost:7191;http://localhost:5096",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"IIS Express": {
|
||||
"commandName": "IISExpress",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
136
DbFirst.BlazorWebApp/Services/CatalogApiClient.cs
Normal file
136
DbFirst.BlazorWebApp/Services/CatalogApiClient.cs
Normal file
@@ -0,0 +1,136 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using DbFirst.BlazorWebApp.Models;
|
||||
|
||||
namespace DbFirst.BlazorWebApp.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)
|
||||
{
|
||||
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
|
||||
{
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
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; }
|
||||
}
|
||||
21
DbFirst.BlazorWebApp/Services/DashboardApiClient.cs
Normal file
21
DbFirst.BlazorWebApp/Services/DashboardApiClient.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using System.Net.Http.Json;
|
||||
using DbFirst.BlazorWebApp.Models;
|
||||
|
||||
namespace DbFirst.BlazorWebApp.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>();
|
||||
}
|
||||
}
|
||||
9
DbFirst.BlazorWebApp/appsettings.Development.json
Normal file
9
DbFirst.BlazorWebApp/appsettings.Development.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"ApiBaseUrl": "https://localhost:7204/"
|
||||
}
|
||||
10
DbFirst.BlazorWebApp/appsettings.json
Normal file
10
DbFirst.BlazorWebApp/appsettings.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"ApiBaseUrl": "https://localhost:7204/",
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
51
DbFirst.BlazorWebApp/wwwroot/app.css
Normal file
51
DbFirst.BlazorWebApp/wwwroot/app.css
Normal file
@@ -0,0 +1,51 @@
|
||||
html, body {
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
a, .btn-link {
|
||||
color: #006bb7;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
h1:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.valid.modified:not([type=checkbox]) {
|
||||
outline: 1px solid #26b050;
|
||||
}
|
||||
|
||||
.invalid {
|
||||
outline: 1px solid #e50000;
|
||||
}
|
||||
|
||||
.validation-message {
|
||||
color: #e50000;
|
||||
}
|
||||
|
||||
.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."
|
||||
}
|
||||
|
||||
.darker-border-checkbox.form-check-input {
|
||||
border-color: #929292;
|
||||
}
|
||||
7
DbFirst.BlazorWebApp/wwwroot/bootstrap/bootstrap.min.css
vendored
Normal file
7
DbFirst.BlazorWebApp/wwwroot/bootstrap/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
BIN
DbFirst.BlazorWebApp/wwwroot/favicon.png
Normal file
BIN
DbFirst.BlazorWebApp/wwwroot/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
7
DbFirst.Domain/CatalogUpdateProcedure.cs
Normal file
7
DbFirst.Domain/CatalogUpdateProcedure.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace DbFirst.Domain;
|
||||
|
||||
public enum CatalogUpdateProcedure
|
||||
{
|
||||
Update = 0,
|
||||
Save = 1
|
||||
}
|
||||
@@ -6,8 +6,4 @@
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Repositories\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
using DbFirst.Domain.Entities;
|
||||
|
||||
namespace DbFirst.Domain.Repositories;
|
||||
|
||||
// TODO: instead of creating interface per entity, consider using generic repository pattern (eg. IRepository<T>) to reduce code duplication.
|
||||
//TODO: move to application layer as a part of clean architecture
|
||||
public interface ICatalogRepository
|
||||
{
|
||||
Task<List<VwmyCatalog>> GetAllAsync(CancellationToken cancellationToken = default);
|
||||
Task<VwmyCatalog?> GetByIdAsync(int id, CancellationToken cancellationToken = default);
|
||||
Task<VwmyCatalog> InsertAsync(VwmyCatalog catalog, CancellationToken cancellationToken = default);
|
||||
Task<VwmyCatalog?> UpdateAsync(int id, VwmyCatalog catalog, CancellationToken cancellationToken = default);
|
||||
Task<bool> DeleteAsync(int id, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -1,49 +1,54 @@
|
||||
using DbFirst.Domain.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace DbFirst.Infrastructure;
|
||||
|
||||
public partial class ApplicationDbContext : DbContext
|
||||
{
|
||||
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
|
||||
private readonly TableConfigurations _config;
|
||||
|
||||
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options, IOptionsMonitor<TableConfigurations> configOptions)
|
||||
: base(options)
|
||||
{
|
||||
_config = configOptions.CurrentValue;
|
||||
}
|
||||
|
||||
public virtual DbSet<VwmyCatalog> VwmyCatalogs { get; set; }
|
||||
|
||||
// TODO: Configure column names on appsettings via IConfiguration
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
var catCfg = _config.VwmyCatalog;
|
||||
|
||||
modelBuilder.Entity<VwmyCatalog>(entity =>
|
||||
{
|
||||
entity.HasKey(e => e.Guid);
|
||||
|
||||
entity.ToView("VWMY_CATALOG");
|
||||
entity.ToView(catCfg.ViewName);
|
||||
|
||||
entity.Property(e => e.Guid).HasColumnName("GUID");
|
||||
entity.Property(e => e.Guid).HasColumnName(catCfg.GuidColumnName);
|
||||
entity.Property(e => e.AddedWho)
|
||||
.HasMaxLength(30)
|
||||
.IsUnicode(false)
|
||||
.HasColumnName("ADDED_WHO");
|
||||
.HasColumnName(catCfg.AddedWhoColumnName);
|
||||
entity.Property(e => e.AddedWhen)
|
||||
.HasColumnType("datetime")
|
||||
.HasColumnName("ADDED_WHEN");
|
||||
.HasColumnName(catCfg.AddedWhenColumnName);
|
||||
entity.Property(e => e.CatString)
|
||||
.HasMaxLength(900)
|
||||
.IsUnicode(false)
|
||||
.HasColumnName("CAT_STRING");
|
||||
.HasColumnName(catCfg.CatStringColumnName);
|
||||
entity.Property(e => e.CatTitle)
|
||||
.HasMaxLength(100)
|
||||
.IsUnicode(false)
|
||||
.HasColumnName("CAT_TITLE");
|
||||
.HasColumnName(catCfg.CatTitleColumnName);
|
||||
entity.Property(e => e.ChangedWhen)
|
||||
.HasColumnType("datetime")
|
||||
.HasColumnName("CHANGED_WHEN");
|
||||
.HasColumnName(catCfg.ChangedWhenColumnName);
|
||||
entity.Property(e => e.ChangedWho)
|
||||
.HasMaxLength(30)
|
||||
.IsUnicode(false)
|
||||
.HasColumnName("CHANGED_WHO");
|
||||
.HasColumnName(catCfg.ChangedWhoColumnName);
|
||||
});
|
||||
|
||||
OnModelCreatingPartial(modelBuilder);
|
||||
|
||||
@@ -15,10 +15,13 @@
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\DbFirst.Domain\DbFirst.Domain.csproj" />
|
||||
<ProjectReference Include="..\DbFirst.Application\DbFirst.Application.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
16
DbFirst.Infrastructure/DependencyInjection.cs
Normal file
16
DbFirst.Infrastructure/DependencyInjection.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace DbFirst.Infrastructure;
|
||||
|
||||
public static class DependencyInjection
|
||||
{
|
||||
public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
services.Configure<TableConfigurations>(configuration.GetSection("TableConfigurations"));
|
||||
services.AddDbContext<ApplicationDbContext>(options =>
|
||||
options.UseSqlServer(configuration.GetConnectionString("DefaultConnection")));
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using DbFirst.Domain.Repositories;
|
||||
using DbFirst.Domain;
|
||||
using DbFirst.Domain.Entities;
|
||||
using DbFirst.Application.Repositories;
|
||||
using Microsoft.Data.SqlClient;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Data;
|
||||
@@ -7,6 +8,14 @@ using System.Data;
|
||||
namespace DbFirst.Infrastructure.Repositories;
|
||||
|
||||
// TODO: instead of creating implementation of repository per entity, consider using generic repository pattern (eg. Repository<T>) to reduce code duplication.
|
||||
|
||||
/* Copilot's Response:
|
||||
|
||||
A generic Repository<T> isn’t really worthwhile here:
|
||||
• Reads from the view are generic, but inserts/updates/deletes go through stored procedures with special parameters/output GUIDs.You’d need lots of exceptions/overrides—little gain.
|
||||
• Operations aren’t symmetric (separate procs for insert/update/delete with output handling and reload), so a one-size-fits-all CRUD pattern doesn’t fit well.
|
||||
• Better to keep the specialized repo.If you want reuse, extract small helpers (e.g., for proc calls/output parameters/reload) instead of forcing a generic repository. */
|
||||
|
||||
public class CatalogRepository : ICatalogRepository
|
||||
{
|
||||
private readonly ApplicationDbContext _db;
|
||||
@@ -26,6 +35,11 @@ public class CatalogRepository : ICatalogRepository
|
||||
return await _db.VwmyCatalogs.AsNoTracking().FirstOrDefaultAsync(x => x.Guid == id, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<VwmyCatalog?> GetByTitleAsync(string title, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _db.VwmyCatalogs.AsNoTracking().FirstOrDefaultAsync(x => x.CatTitle == title, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<VwmyCatalog> InsertAsync(VwmyCatalog catalog, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var guidParam = new SqlParameter("@GUID", SqlDbType.Int)
|
||||
@@ -57,26 +71,45 @@ public class CatalogRepository : ICatalogRepository
|
||||
return created;
|
||||
}
|
||||
|
||||
public async Task<VwmyCatalog?> UpdateAsync(int id, VwmyCatalog catalog, CancellationToken cancellationToken = default)
|
||||
public async Task<VwmyCatalog?> UpdateAsync(int id, VwmyCatalog catalog, CatalogUpdateProcedure procedure, CancellationToken cancellationToken = default)
|
||||
{
|
||||
catalog.Guid = id;
|
||||
|
||||
var guidParam = new SqlParameter("@GUID", SqlDbType.Int)
|
||||
{
|
||||
Direction = ParameterDirection.Input,
|
||||
Value = id
|
||||
Direction = ParameterDirection.Output
|
||||
};
|
||||
|
||||
var catTitleParam = new SqlParameter("@CAT_TITLE", catalog.CatTitle);
|
||||
var catStringParam = new SqlParameter("@CAT_STRING", catalog.CatString);
|
||||
var changedWhoParam = new SqlParameter("@CHANGED_WHO", (object?)catalog.ChangedWho ?? DBNull.Value);
|
||||
|
||||
var procName = procedure == CatalogUpdateProcedure.Save
|
||||
? "PRTBMY_CATALOG_SAVE"
|
||||
: "PRTBMY_CATALOG_UPDATE";
|
||||
|
||||
await _db.Database.ExecuteSqlRawAsync(
|
||||
"EXEC dbo.PRTBMY_CATALOG_UPDATE @CAT_TITLE, @CAT_STRING, @CHANGED_WHO, @GUID",
|
||||
$"EXEC dbo.{procName} @CAT_TITLE, @CAT_STRING, @CHANGED_WHO, @GUID OUTPUT",
|
||||
parameters: new[] { catTitleParam, catStringParam, changedWhoParam, guidParam },
|
||||
cancellationToken: cancellationToken);
|
||||
|
||||
return await _db.VwmyCatalogs.AsNoTracking().FirstOrDefaultAsync(x => x.Guid == id, cancellationToken);
|
||||
if (guidParam.Value == DBNull.Value)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var guid = (int)guidParam.Value;
|
||||
if (guid == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return await _db.VwmyCatalogs.AsNoTracking().FirstOrDefaultAsync(x => x.Guid == guid, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<VwmyCatalog?> UpdateAsync(int id, VwmyCatalog catalog, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await UpdateAsync(id, catalog, CatalogUpdateProcedure.Update, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteAsync(int id, CancellationToken cancellationToken = default)
|
||||
|
||||
19
DbFirst.Infrastructure/TableConfigurations.cs
Normal file
19
DbFirst.Infrastructure/TableConfigurations.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
namespace DbFirst.Infrastructure
|
||||
{
|
||||
public class TableConfigurations
|
||||
{
|
||||
public VwmyCatalogConfiguration VwmyCatalog { get; set; } = new();
|
||||
}
|
||||
|
||||
public class VwmyCatalogConfiguration
|
||||
{
|
||||
public string ViewName { get; set; } = "VWMY_CATALOG";
|
||||
public string GuidColumnName { get; set; } = "GUID";
|
||||
public string CatTitleColumnName { get; set; } = "CAT_TITLE";
|
||||
public string CatStringColumnName { get; set; } = "CAT_STRING";
|
||||
public string AddedWhoColumnName { get; set; } = "ADDED_WHO";
|
||||
public string AddedWhenColumnName { get; set; } = "ADDED_WHEN";
|
||||
public string ChangedWhoColumnName { get; set; } = "CHANGED_WHO";
|
||||
public string ChangedWhenColumnName { get; set; } = "CHANGED_WHEN";
|
||||
}
|
||||
}
|
||||
14
DbFirst.sln
14
DbFirst.sln
@@ -13,6 +13,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DbFirst.Domain", "DbFirst.D
|
||||
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}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -83,6 +85,18 @@ Global
|
||||
{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.Build.0 = Debug|Any CPU
|
||||
{FF1D77C4-A13D-43E0-BCE1-C18C01F1767C}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{FF1D77C4-A13D-43E0-BCE1-C18C01F1767C}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{FF1D77C4-A13D-43E0-BCE1-C18C01F1767C}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{FF1D77C4-A13D-43E0-BCE1-C18C01F1767C}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{FF1D77C4-A13D-43E0-BCE1-C18C01F1767C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{FF1D77C4-A13D-43E0-BCE1-C18C01F1767C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{FF1D77C4-A13D-43E0-BCE1-C18C01F1767C}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{FF1D77C4-A13D-43E0-BCE1-C18C01F1767C}.Release|x64.Build.0 = Release|Any CPU
|
||||
{FF1D77C4-A13D-43E0-BCE1-C18C01F1767C}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{FF1D77C4-A13D-43E0-BCE1-C18C01F1767C}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
||||
Reference in New Issue
Block a user