Remove BlazorWasm project and all related frontend assets
This commit deletes the entire Blazor WebAssembly frontend project, including all source code, components, DTOs, services, CSS, configuration files, static assets (such as favicon and icons), and the main HTML entry point. All references to `DbFirst.BlazorWasm` have been removed from the solution file, and associated dependencies like Bootstrap CSS and its source map are also deleted. This effectively removes the BlazorWasm application from the repository.
This commit is contained in:
@@ -1,60 +0,0 @@
|
||||
/*
|
||||
Ablauf und die Rolle jeder Datei in der Blazor WebAssembly-Anwendung:
|
||||
|
||||
1.index.html
|
||||
• Reihenfolge: Wird als erstes geladen, wenn die Anwendung im Browser geöffnet wird.
|
||||
• Purpose:
|
||||
• Lädt die Blazor WebAssembly-Umgebung (blazor.webassembly.js).
|
||||
• Definiert den Platzhalter <div id="app">, in dem die Blazor-Komponenten gerendert werden.
|
||||
• Bindet Stylesheets und Skripte ein, die für das Styling und die Funktionalität der Anwendung benötigt werden.
|
||||
|
||||
2. Program.cs
|
||||
• Reihenfolge: Wird nach index.html ausgeführt, sobald die Blazor-Umgebung initialisiert ist.
|
||||
• Purpose:
|
||||
• Initialisiert die Blazor WebAssembly-Anwendung.
|
||||
• Registriert Root-Komponenten (App) und Abhängigkeiten (z. B. HttpClient, CatalogApiClient).
|
||||
• Konfiguriert die Basis-URL für API-Aufrufe.
|
||||
|
||||
3. App.razor
|
||||
• Reihenfolge: Wird als nächstes geladen, nachdem die Anwendung initialisiert wurde.
|
||||
• Purpose:
|
||||
• Definiert die Routing-Logik der Anwendung.
|
||||
• Entscheidet, welche Komponente basierend auf der URL gerendert wird.
|
||||
• Stellt sicher, dass ein Standardlayout (MainLayout) verwendet wird.
|
||||
|
||||
4.MainLayout.razor
|
||||
• Reihenfolge: Wird geladen, wenn eine Seite gerendert wird, da es das Standardlayout ist.
|
||||
• Purpose:
|
||||
• Definiert das Hauptlayout der Anwendung.
|
||||
• Enthält die Navigationsleiste (NavMenu) und den Platzhalter für den Seiteninhalt (@Body).
|
||||
|
||||
5. NavMenu.razor
|
||||
• Reihenfolge: Wird als Teil des Layouts (MainLayout) geladen.
|
||||
• Purpose:
|
||||
• Stellt die Navigationsleiste bereit.
|
||||
• Enthält Links zu verschiedenen Seiten der Anwendung (z. B. Home, Catalogs).
|
||||
• Ermöglicht das Ein- und Ausklappen des Menüs.
|
||||
|
||||
6. Catalogs.razor
|
||||
• Reihenfolge: Wird geladen, wenn der Benutzer die URL /catalogs aufruft.
|
||||
• Purpose:
|
||||
• Stellt die Benutzeroberfläche für die Verwaltung von Katalogen bereit.
|
||||
• Nutzt CatalogApiClient, um Daten von der API zu laden, zu erstellen, zu aktualisieren oder zu löschen.
|
||||
• Verwendet DevExpress-Komponenten für ein modernes UI.
|
||||
|
||||
7. CatalogApiClient.cs
|
||||
• Reihenfolge: Wird verwendet, wenn Catalogs.razor API-Aufrufe ausführt.
|
||||
• Purpose:
|
||||
• Kapselt die Kommunikation mit der API.
|
||||
• Bietet Methoden für CRUD-Operationen (Create, Read, Update, Delete) auf Katalog-Daten.
|
||||
• Behandelt Fehler und gibt benutzerfreundliche Fehlermeldungen zurück.
|
||||
|
||||
Zusammenfassung des Ablaufs:
|
||||
1.index.html: Lädt die Blazor-Umgebung und startet die Anwendung.
|
||||
2. Program.cs: Initialisiert die Anwendung und registriert Abhängigkeiten.
|
||||
3. App.razor: Definiert die Routing-Logik und lädt das Standardlayout.
|
||||
4. MainLayout.razor: Stellt das Hauptlayout bereit.
|
||||
5. NavMenu.razor: Lädt die Navigationsleiste.
|
||||
6. Seiten wie Catalogs.razor: Werden basierend auf der URL gerendert.
|
||||
7. CatalogApiClient.cs: Führt API-Aufrufe aus, wenn die Seite Daten benötigt.
|
||||
*/
|
||||
@@ -1,20 +0,0 @@
|
||||
@*
|
||||
• Ist der logische Einstiegspunkt der Blazor-Anwendung.
|
||||
• Sie definiert die Routing-Logik und das Standardlayout der Anwendung.
|
||||
• Der Router-Komponent in App.razor entscheidet, welche Blazor-Komponente basierend auf der URL geladen wird.
|
||||
kurz: Steuert die Navigation und das Rendering der Blazor-Komponenten.
|
||||
*@
|
||||
@DxResourceManager.RegisterTheme(Themes.Fluent)
|
||||
@DxResourceManager.RegisterScripts()
|
||||
<Router AppAssembly="@typeof(App).Assembly">
|
||||
<Found Context="routeData">
|
||||
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
|
||||
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
|
||||
</Found>
|
||||
<NotFound>
|
||||
<PageTitle>Not found</PageTitle>
|
||||
<LayoutView Layout="@typeof(MainLayout)">
|
||||
<p role="alert">Sorry, there's nothing at this address.</p>
|
||||
</LayoutView>
|
||||
</NotFound>
|
||||
</Router>
|
||||
@@ -1,808 +0,0 @@
|
||||
@using System.Text.Json
|
||||
@using Microsoft.AspNetCore.Components
|
||||
@using Microsoft.AspNetCore.Components.Rendering
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using DevExpress.Blazor
|
||||
@using DevExpress.Data.Filtering
|
||||
@inject CatalogApiClient Api
|
||||
@inject LayoutApiClient LayoutApi
|
||||
@inject IJSRuntime JsRuntime
|
||||
|
||||
<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;
|
||||
}
|
||||
.catalog-edit-popup {
|
||||
min-width: 720px;
|
||||
}
|
||||
.band-editor {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.band-controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
.band-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
.band-columns {
|
||||
max-width: 720px;
|
||||
}
|
||||
.filter-row-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.filter-operator {
|
||||
width: 52px;
|
||||
min-width: 52px;
|
||||
flex: 0 0 52px;
|
||||
}
|
||||
.filter-value {
|
||||
min-width: 160px;
|
||||
flex: 1 1 160px;
|
||||
}
|
||||
.loading-container {
|
||||
min-height: 160px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(errorMessage))
|
||||
{
|
||||
<div class="alert alert-danger" role="alert">@errorMessage</div>
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(infoMessage))
|
||||
{
|
||||
<div class="alert alert-success" role="alert">@infoMessage</div>
|
||||
}
|
||||
|
||||
@if (!hasLoaded || isLoading)
|
||||
{
|
||||
<div class="loading-container">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">Lade...</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else if (items.Count == 0)
|
||||
{
|
||||
<p>Keine Einträge vorhanden.</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="band-editor">
|
||||
<div class="band-controls">
|
||||
<DxButton Text="Band hinzufügen" Click="AddBand" />
|
||||
<DxButton Text="Layout speichern" Click="SaveLayoutAsync" Enabled="@CanSaveBandLayout" />
|
||||
<DxButton Text="Band-Layout zurücksetzen" Click="ResetBandLayoutAsync" />
|
||||
</div>
|
||||
@foreach (var band in bandLayout.Bands)
|
||||
{
|
||||
<div class="band-row">
|
||||
<DxTextBox Text="@band.Caption" TextChanged="@(value => UpdateBandCaption(band, value))" />
|
||||
<DxButton Text="Entfernen" Click="@(() => RemoveBand(band))" />
|
||||
</div>
|
||||
}
|
||||
<DxFormLayout CssClass="band-columns" ColCount="2">
|
||||
@foreach (var column in columnDefinitions)
|
||||
{
|
||||
<DxFormLayoutItem Caption="@column.Caption">
|
||||
<DxComboBox Data="@bandOptions"
|
||||
TData="BandOption"
|
||||
TValue="string"
|
||||
TextFieldName="Caption"
|
||||
ValueFieldName="Id"
|
||||
Value="@GetColumnBand(column.FieldName)"
|
||||
ValueChanged="@(value => UpdateColumnBand(column.FieldName, value))"
|
||||
Width="100%" />
|
||||
</DxFormLayoutItem>
|
||||
}
|
||||
</DxFormLayout>
|
||||
</div>
|
||||
|
||||
<div class="grid-section">
|
||||
<DxGrid Data="@items"
|
||||
TItem="CatalogReadDto"
|
||||
KeyFieldName="@nameof(CatalogReadDto.Guid)"
|
||||
ShowGroupPanel="true"
|
||||
ShowGroupedColumns="true"
|
||||
AllowGroup="true"
|
||||
FilterMenuButtonDisplayMode="GridFilterMenuButtonDisplayMode.Always"
|
||||
AllowColumnResize="true"
|
||||
ColumnResizeMode="GridColumnResizeMode.ColumnsContainer"
|
||||
AllowColumnReorder="true"
|
||||
PageSize="10"
|
||||
CssClass="mb-4 catalog-grid"
|
||||
EditMode="GridEditMode.PopupEditForm"
|
||||
PopupEditFormCssClass="catalog-edit-popup"
|
||||
PopupEditFormHeaderText="@popupHeaderText"
|
||||
CustomizeEditModel="OnCustomizeEditModel"
|
||||
EditModelSaving="OnEditModelSaving"
|
||||
DataItemDeleting="OnDataItemDeleting"
|
||||
@ref="gridRef">
|
||||
<Columns>
|
||||
@RenderColumns()
|
||||
</Columns>
|
||||
<EditFormTemplate Context="editFormContext">
|
||||
@{ SetEditContext(editFormContext.EditContext); var editModel = (CatalogEditModel)editFormContext.EditModel; SetPopupHeaderText(editModel.IsNew); }
|
||||
<DxFormLayout ColCount="2">
|
||||
<DxFormLayoutItem Caption="Titel">
|
||||
<DxTextBox @bind-Text="editModel.CatTitle" Width="100%" />
|
||||
</DxFormLayoutItem>
|
||||
<DxFormLayoutItem Caption="Kennung">
|
||||
<DxTextBox @bind-Text="editModel.CatString" Width="100%" />
|
||||
</DxFormLayoutItem>
|
||||
@if (!editModel.IsNew)
|
||||
{
|
||||
<DxFormLayoutItem Caption="Update-Prozedur">
|
||||
<DxComboBox Data="@procedureOptions"
|
||||
TData="ProcedureOption"
|
||||
TValue="int"
|
||||
TextFieldName="Text"
|
||||
ValueFieldName="Value"
|
||||
@bind-Value="editModel.UpdateProcedure"
|
||||
Width="100%" />
|
||||
</DxFormLayoutItem>
|
||||
}
|
||||
<DxFormLayoutItem ColSpanMd="12">
|
||||
<ValidationSummary />
|
||||
</DxFormLayoutItem>
|
||||
</DxFormLayout>
|
||||
</EditFormTemplate>
|
||||
</DxGrid>
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
private List<CatalogReadDto> items = new();
|
||||
private bool isLoading;
|
||||
private bool hasLoaded;
|
||||
private string? errorMessage;
|
||||
private string? infoMessage;
|
||||
private EditContext? editContext;
|
||||
private ValidationMessageStore? validationMessageStore;
|
||||
private IGrid? gridRef;
|
||||
private string popupHeaderText = "Edit";
|
||||
private const string LayoutType = "GRID_BANDS";
|
||||
private const string LayoutKey = "CatalogsGrid";
|
||||
private const string LayoutUserStorageKey = "layoutUser";
|
||||
private string? layoutUser;
|
||||
private BandLayout bandLayout = new();
|
||||
private Dictionary<string, string> columnBandAssignments = new();
|
||||
private List<BandOption> bandOptions = new();
|
||||
private Dictionary<string, ColumnDefinition> columnLookup = new();
|
||||
private readonly JsonSerializerOptions jsonOptions = new(JsonSerializerDefaults.Web);
|
||||
private List<ColumnDefinition> columnDefinitions = new()
|
||||
{
|
||||
new() { FieldName = nameof(CatalogReadDto.Guid), Caption = "Id", Width = "140px", FilterType = ColumnFilterType.Text },
|
||||
new() { FieldName = nameof(CatalogReadDto.CatTitle), Caption = "Titel", FilterType = ColumnFilterType.Text },
|
||||
new() { FieldName = nameof(CatalogReadDto.CatString), Caption = "String", FilterType = ColumnFilterType.Text },
|
||||
new() { FieldName = nameof(CatalogReadDto.AddedWho), Caption = "Angelegt von", ReadOnly = true, FilterType = ColumnFilterType.Text },
|
||||
new() { FieldName = nameof(CatalogReadDto.AddedWhen), Caption = "Angelegt am", ReadOnly = true, FilterType = ColumnFilterType.Date },
|
||||
new() { FieldName = nameof(CatalogReadDto.ChangedWho), Caption = "Geändert von", ReadOnly = true, FilterType = ColumnFilterType.Text },
|
||||
new() { FieldName = nameof(CatalogReadDto.ChangedWhen), Caption = "Geändert am", ReadOnly = true, FilterType = ColumnFilterType.Date }
|
||||
};
|
||||
|
||||
private readonly List<ProcedureOption> procedureOptions = new()
|
||||
{
|
||||
new() { Value = 0, Text = "PRTBMY_CATALOG_UPDATE" },
|
||||
new() { Value = 1, Text = "PRTBMY_CATALOG_SAVE" }
|
||||
};
|
||||
|
||||
private bool CanSaveBandLayout => !string.IsNullOrWhiteSpace(layoutUser);
|
||||
private bool gridLayoutApplied;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
columnLookup = columnDefinitions.ToDictionary(column => column.FieldName, StringComparer.OrdinalIgnoreCase);
|
||||
await EnsureLayoutUserAsync();
|
||||
await LoadBandLayoutAsync();
|
||||
await LoadCatalogs();
|
||||
}
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (!gridLayoutApplied && gridRef != null && bandLayout.GridLayout != null)
|
||||
{
|
||||
gridRef.LoadLayout(bandLayout.GridLayout);
|
||||
gridLayoutApplied = true;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
}
|
||||
|
||||
private void SetEditContext(EditContext context)
|
||||
{
|
||||
if (editContext == context)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (editContext != null)
|
||||
{
|
||||
editContext.OnFieldChanged -= OnEditFieldChanged;
|
||||
}
|
||||
|
||||
editContext = context;
|
||||
validationMessageStore = new ValidationMessageStore(editContext);
|
||||
editContext.OnFieldChanged += OnEditFieldChanged;
|
||||
}
|
||||
|
||||
private void OnEditFieldChanged(object? sender, FieldChangedEventArgs e)
|
||||
{
|
||||
if (validationMessageStore == null || editContext == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.FieldIdentifier.FieldName == nameof(CatalogEditModel.UpdateProcedure))
|
||||
{
|
||||
validationMessageStore.Clear();
|
||||
editContext.NotifyValidationStateChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.FieldIdentifier.FieldName == nameof(CatalogEditModel.CatTitle))
|
||||
{
|
||||
var field = new FieldIdentifier(editContext.Model, nameof(CatalogEditModel.CatTitle));
|
||||
validationMessageStore.Clear(field);
|
||||
editContext.NotifyValidationStateChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private void SetPopupHeaderText(bool isNew)
|
||||
{
|
||||
popupHeaderText = isNew ? "Neu" : "Edit";
|
||||
}
|
||||
|
||||
private void OnCustomizeEditModel(GridCustomizeEditModelEventArgs e)
|
||||
{
|
||||
popupHeaderText = e.IsNew ? "Neu" : "Edit";
|
||||
if (e.IsNew)
|
||||
{
|
||||
e.EditModel = new CatalogEditModel { IsNew = true };
|
||||
return;
|
||||
}
|
||||
|
||||
var item = (CatalogReadDto)e.DataItem;
|
||||
e.EditModel = new CatalogEditModel
|
||||
{
|
||||
Guid = item.Guid,
|
||||
CatTitle = item.CatTitle,
|
||||
CatString = item.CatString,
|
||||
UpdateProcedure = 0,
|
||||
OriginalCatTitle = item.CatTitle,
|
||||
IsNew = false
|
||||
};
|
||||
}
|
||||
|
||||
private async Task LoadCatalogs()
|
||||
{
|
||||
isLoading = true;
|
||||
errorMessage = null;
|
||||
try
|
||||
{
|
||||
items = await Api.GetAllAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errorMessage = $"Kataloge konnten nicht geladen werden: {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
isLoading = false;
|
||||
hasLoaded = true;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnEditModelSaving(GridEditModelSavingEventArgs e)
|
||||
{
|
||||
errorMessage = null;
|
||||
infoMessage = null;
|
||||
|
||||
validationMessageStore?.Clear();
|
||||
editContext?.NotifyValidationStateChanged();
|
||||
|
||||
var editModel = (CatalogEditModel)e.EditModel;
|
||||
if (!ValidateEditModel(editModel, e.IsNew))
|
||||
{
|
||||
e.Cancel = true;
|
||||
return;
|
||||
}
|
||||
|
||||
var dto = new CatalogWriteDto
|
||||
{
|
||||
CatTitle = editModel.CatTitle,
|
||||
CatString = editModel.CatString,
|
||||
UpdateProcedure = editModel.UpdateProcedure
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
if (e.IsNew)
|
||||
{
|
||||
var created = await Api.CreateAsync(dto);
|
||||
if (!created.Success || created.Value == null)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(created.Error))
|
||||
{
|
||||
AddValidationError(editModel, nameof(CatalogEditModel.CatTitle), created.Error);
|
||||
}
|
||||
else
|
||||
{
|
||||
errorMessage = "Anlegen fehlgeschlagen.";
|
||||
}
|
||||
|
||||
e.Cancel = true;
|
||||
return;
|
||||
}
|
||||
|
||||
infoMessage = "Katalog angelegt.";
|
||||
}
|
||||
else
|
||||
{
|
||||
var updated = await Api.UpdateAsync(editModel.Guid, dto);
|
||||
if (!updated.Success)
|
||||
{
|
||||
errorMessage = updated.Error ?? "Aktualisierung fehlgeschlagen.";
|
||||
e.Cancel = true;
|
||||
return;
|
||||
}
|
||||
|
||||
infoMessage = "Katalog aktualisiert.";
|
||||
}
|
||||
|
||||
await LoadCatalogs();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errorMessage = $"Fehler beim Speichern: {ex.Message}";
|
||||
e.Cancel = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void AddValidationError(CatalogEditModel editModel, string fieldName, string message)
|
||||
{
|
||||
if (editContext == null || validationMessageStore == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var field = new FieldIdentifier(editModel, fieldName);
|
||||
validationMessageStore.Add(field, message);
|
||||
editContext.NotifyValidationStateChanged();
|
||||
}
|
||||
|
||||
private bool ValidateEditModel(CatalogEditModel editModel, bool isNew)
|
||||
{
|
||||
if (isNew)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (editModel.UpdateProcedure == 0 &&
|
||||
!string.Equals(editModel.CatTitle, editModel.OriginalCatTitle, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
AddValidationError(editModel, nameof(CatalogEditModel.CatTitle), "Titel kann nicht geändert werden.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task OnDataItemDeleting(GridDataItemDeletingEventArgs e)
|
||||
{
|
||||
errorMessage = null;
|
||||
infoMessage = null;
|
||||
|
||||
var item = (CatalogReadDto)e.DataItem;
|
||||
|
||||
try
|
||||
{
|
||||
var deleted = await Api.DeleteAsync(item.Guid);
|
||||
if (!deleted.Success)
|
||||
{
|
||||
errorMessage = deleted.Error ?? "Löschen fehlgeschlagen.";
|
||||
e.Cancel = true;
|
||||
return;
|
||||
}
|
||||
|
||||
infoMessage = "Katalog gelöscht.";
|
||||
await LoadCatalogs();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errorMessage = $"Fehler beim Löschen: {ex.Message}";
|
||||
e.Cancel = true;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task EnsureLayoutUserAsync()
|
||||
{
|
||||
layoutUser = await JsRuntime.InvokeAsync<string?>("localStorage.getItem", LayoutUserStorageKey);
|
||||
if (string.IsNullOrWhiteSpace(layoutUser))
|
||||
{
|
||||
layoutUser = Guid.NewGuid().ToString("N");
|
||||
await JsRuntime.InvokeVoidAsync("localStorage.setItem", LayoutUserStorageKey, layoutUser);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadBandLayoutAsync()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(layoutUser))
|
||||
{
|
||||
bandLayout = new BandLayout();
|
||||
UpdateBandOptions();
|
||||
return;
|
||||
}
|
||||
|
||||
var stored = await LayoutApi.GetAsync(LayoutType, LayoutKey, layoutUser);
|
||||
if (stored != null && !string.IsNullOrWhiteSpace(stored.LayoutData))
|
||||
{
|
||||
var parsed = JsonSerializer.Deserialize<BandLayout>(stored.LayoutData, jsonOptions);
|
||||
bandLayout = NormalizeBandLayout(parsed);
|
||||
}
|
||||
else
|
||||
{
|
||||
bandLayout = new BandLayout();
|
||||
}
|
||||
|
||||
columnBandAssignments = BuildAssignmentsFromLayout(bandLayout);
|
||||
ApplyColumnLayoutFromStorage();
|
||||
UpdateBandOptions();
|
||||
}
|
||||
|
||||
private async Task SaveLayoutAsync()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(layoutUser))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
CaptureColumnLayoutFromGrid();
|
||||
|
||||
var layoutData = JsonSerializer.Serialize(bandLayout, jsonOptions);
|
||||
await LayoutApi.UpsertAsync(new LayoutDto
|
||||
{
|
||||
LayoutType = LayoutType,
|
||||
LayoutKey = LayoutKey,
|
||||
UserName = layoutUser,
|
||||
LayoutData = layoutData
|
||||
});
|
||||
infoMessage = "Layout gespeichert.";
|
||||
errorMessage = null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errorMessage = $"Layout konnte nicht gespeichert werden: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
private void CaptureColumnLayoutFromGrid()
|
||||
{
|
||||
if (gridRef == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var layout = gridRef.SaveLayout();
|
||||
bandLayout.GridLayout = layout;
|
||||
|
||||
var orderedColumns = layout.Columns
|
||||
.Where(column => !string.IsNullOrWhiteSpace(column.FieldName))
|
||||
.OrderBy(column => column.VisibleIndex)
|
||||
.ToList();
|
||||
|
||||
bandLayout.ColumnOrder = orderedColumns
|
||||
.Select(column => column.FieldName)
|
||||
.ToList();
|
||||
|
||||
bandLayout.ColumnWidths = orderedColumns
|
||||
.Where(column => !string.IsNullOrWhiteSpace(column.Width))
|
||||
.ToDictionary(column => column.FieldName, column => column.Width, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private async Task ResetBandLayoutAsync()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(layoutUser))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await LayoutApi.DeleteAsync(LayoutType, LayoutKey, layoutUser);
|
||||
bandLayout = new BandLayout();
|
||||
columnBandAssignments.Clear();
|
||||
UpdateBandOptions();
|
||||
infoMessage = "Band-Layout zurückgesetzt.";
|
||||
}
|
||||
|
||||
private void ApplyColumnLayoutFromStorage()
|
||||
{
|
||||
foreach (var column in columnDefinitions)
|
||||
{
|
||||
if (bandLayout.ColumnWidths.TryGetValue(column.FieldName, out var width) && !string.IsNullOrWhiteSpace(width))
|
||||
{
|
||||
column.Width = width;
|
||||
}
|
||||
}
|
||||
|
||||
columnLookup = columnDefinitions.ToDictionary(column => column.FieldName, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private void AddBand()
|
||||
{
|
||||
bandLayout.Bands.Add(new BandDefinition
|
||||
{
|
||||
Id = Guid.NewGuid().ToString("N"),
|
||||
Caption = "Band"
|
||||
});
|
||||
UpdateBandOptions();
|
||||
}
|
||||
|
||||
private void RemoveBand(BandDefinition band)
|
||||
{
|
||||
bandLayout.Bands.Remove(band);
|
||||
var removedColumns = columnBandAssignments.Where(pair => pair.Value == band.Id)
|
||||
.Select(pair => pair.Key)
|
||||
.ToList();
|
||||
foreach (var column in removedColumns)
|
||||
{
|
||||
columnBandAssignments.Remove(column);
|
||||
}
|
||||
UpdateBandOptions();
|
||||
SyncBandsFromAssignments();
|
||||
}
|
||||
|
||||
private void UpdateBandCaption(BandDefinition band, string value)
|
||||
{
|
||||
band.Caption = value;
|
||||
UpdateBandOptions();
|
||||
}
|
||||
|
||||
private void UpdateColumnBand(string fieldName, string? bandId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(bandId))
|
||||
{
|
||||
columnBandAssignments.Remove(fieldName);
|
||||
}
|
||||
else
|
||||
{
|
||||
columnBandAssignments[fieldName] = bandId;
|
||||
}
|
||||
|
||||
SyncBandsFromAssignments();
|
||||
}
|
||||
|
||||
private string GetColumnBand(string fieldName)
|
||||
{
|
||||
return columnBandAssignments.TryGetValue(fieldName, out var bandId) ? bandId : string.Empty;
|
||||
}
|
||||
|
||||
private void SyncBandsFromAssignments()
|
||||
{
|
||||
foreach (var band in bandLayout.Bands)
|
||||
{
|
||||
band.Columns = columnDefinitions
|
||||
.Where(column => columnBandAssignments.TryGetValue(column.FieldName, out var bandId) && bandId == band.Id)
|
||||
.Select(column => column.FieldName)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private void UpdateBandOptions()
|
||||
{
|
||||
bandOptions = new List<BandOption> { new() { Id = string.Empty, Caption = "Ohne Band" } };
|
||||
bandOptions.AddRange(bandLayout.Bands.Select(band => new BandOption { Id = band.Id, Caption = band.Caption }));
|
||||
}
|
||||
|
||||
private BandLayout NormalizeBandLayout(BandLayout? layout)
|
||||
{
|
||||
layout ??= new BandLayout();
|
||||
layout.Bands ??= new List<BandDefinition>();
|
||||
layout.ColumnOrder ??= new List<string>();
|
||||
layout.ColumnWidths ??= new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var band in layout.Bands)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(band.Id))
|
||||
{
|
||||
band.Id = Guid.NewGuid().ToString("N");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(band.Caption))
|
||||
{
|
||||
band.Caption = "Band";
|
||||
}
|
||||
|
||||
band.Columns = band.Columns?.Where(columnLookup.ContainsKey).ToList() ?? new List<string>();
|
||||
}
|
||||
|
||||
return layout;
|
||||
}
|
||||
|
||||
private Dictionary<string, string> BuildAssignmentsFromLayout(BandLayout layout)
|
||||
{
|
||||
var assignments = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var band in layout.Bands)
|
||||
{
|
||||
foreach (var column in band.Columns)
|
||||
{
|
||||
assignments[column] = band.Id;
|
||||
}
|
||||
}
|
||||
|
||||
return assignments;
|
||||
}
|
||||
|
||||
private RenderFragment RenderColumns() => builder =>
|
||||
{
|
||||
var seq = 0;
|
||||
builder.OpenComponent<DxGridCommandColumn>(seq++);
|
||||
builder.AddAttribute(seq++, "Width", "120px");
|
||||
builder.CloseComponent();
|
||||
|
||||
var grouped = bandLayout.Bands.SelectMany(band => band.Columns).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var column in columnDefinitions.Where(column => !grouped.Contains(column.FieldName)))
|
||||
{
|
||||
BuildDataColumn(builder, ref seq, column);
|
||||
}
|
||||
|
||||
foreach (var band in bandLayout.Bands)
|
||||
{
|
||||
if (band.Columns.Count == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
builder.OpenComponent<DxGridBandColumn>(seq++);
|
||||
builder.AddAttribute(seq++, "Caption", band.Caption);
|
||||
builder.AddAttribute(seq++, "Columns", (RenderFragment)(bandBuilder =>
|
||||
{
|
||||
var bandSeq = 0;
|
||||
foreach (var columnName in band.Columns)
|
||||
{
|
||||
if (columnLookup.TryGetValue(columnName, out var column))
|
||||
{
|
||||
BuildDataColumn(bandBuilder, ref bandSeq, column);
|
||||
}
|
||||
}
|
||||
}));
|
||||
builder.CloseComponent();
|
||||
}
|
||||
};
|
||||
|
||||
private void BuildDataColumn(RenderTreeBuilder builder, ref int seq, ColumnDefinition column)
|
||||
{
|
||||
builder.OpenComponent<DxGridDataColumn>(seq++);
|
||||
builder.AddAttribute(seq++, "FieldName", column.FieldName);
|
||||
builder.AddAttribute(seq++, "Caption", column.Caption);
|
||||
if (!string.IsNullOrWhiteSpace(column.Width))
|
||||
{
|
||||
builder.AddAttribute(seq++, "Width", column.Width);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(column.DisplayFormat))
|
||||
{
|
||||
builder.AddAttribute(seq++, "DisplayFormat", column.DisplayFormat);
|
||||
}
|
||||
|
||||
if (column.ReadOnly)
|
||||
{
|
||||
builder.AddAttribute(seq++, "ReadOnly", true);
|
||||
}
|
||||
|
||||
builder.CloseComponent();
|
||||
}
|
||||
|
||||
private sealed class BandLayout
|
||||
{
|
||||
public List<BandDefinition> Bands { get; set; } = new();
|
||||
public List<string> ColumnOrder { get; set; } = new();
|
||||
public Dictionary<string, string?> ColumnWidths { get; set; } = new(StringComparer.OrdinalIgnoreCase);
|
||||
public GridPersistentLayout? GridLayout { get; set; }
|
||||
}
|
||||
|
||||
private sealed class BandDefinition
|
||||
{
|
||||
public string Id { get; set; } = string.Empty;
|
||||
public string Caption { get; set; } = string.Empty;
|
||||
public List<string> Columns { get; set; } = new();
|
||||
}
|
||||
|
||||
private sealed class BandOption
|
||||
{
|
||||
public string Id { get; set; } = string.Empty;
|
||||
public string Caption { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
private sealed class ColumnDefinition
|
||||
{
|
||||
public string FieldName { get; init; } = string.Empty;
|
||||
public string Caption { get; init; } = string.Empty;
|
||||
public string? Width { get; set; }
|
||||
public string? DisplayFormat { get; init; }
|
||||
public bool ReadOnly { get; init; }
|
||||
public ColumnFilterType FilterType { get; init; }
|
||||
}
|
||||
|
||||
private enum ColumnFilterType
|
||||
{
|
||||
Text,
|
||||
Date
|
||||
}
|
||||
|
||||
private sealed class CatalogEditModel
|
||||
{
|
||||
public int Guid { get; set; }
|
||||
public string CatTitle { get; set; } = string.Empty;
|
||||
public string CatString { get; set; } = string.Empty;
|
||||
public int UpdateProcedure { get; set; }
|
||||
public string OriginalCatTitle { get; set; } = string.Empty;
|
||||
public bool IsNew { get; set; }
|
||||
}
|
||||
|
||||
private sealed class ProcedureOption
|
||||
{
|
||||
public int Value { get; set; }
|
||||
public string Text { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
private sealed class FilterOperatorOption
|
||||
{
|
||||
public string Value { get; set; } = string.Empty;
|
||||
public string Text { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -1,800 +0,0 @@
|
||||
@using System.Text.Json
|
||||
@using Microsoft.AspNetCore.Components
|
||||
@using Microsoft.AspNetCore.Components.Rendering
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using DevExpress.Blazor
|
||||
@using DevExpress.Data.Filtering
|
||||
@inject MassDataApiClient Api
|
||||
@inject LayoutApiClient LayoutApi
|
||||
@inject IJSRuntime JsRuntime
|
||||
|
||||
<style>
|
||||
.action-panel { margin-bottom: 16px; }
|
||||
.grid-section { margin-top: 12px; }
|
||||
.pager-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.page-size-selector {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
.page-size-label {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.page-size-combo {
|
||||
width: 13ch;
|
||||
min-width: 13ch;
|
||||
max-width: 13ch;
|
||||
}
|
||||
.page-size-combo input {
|
||||
text-align: left;
|
||||
}
|
||||
.massdata-edit-popup {
|
||||
min-width: 720px;
|
||||
}
|
||||
.band-editor {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.band-controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
.band-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
.band-columns {
|
||||
max-width: 720px;
|
||||
}
|
||||
.loading-container {
|
||||
min-height: 160px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(errorMessage))
|
||||
{
|
||||
<div class="alert alert-danger" role="alert">@errorMessage</div>
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(infoMessage))
|
||||
{
|
||||
<div class="alert alert-success" role="alert">@infoMessage</div>
|
||||
}
|
||||
|
||||
@if (!hasLoaded || isLoading)
|
||||
{
|
||||
<div class="loading-container">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">Lade...</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else if (items.Count == 0)
|
||||
{
|
||||
<p>Keine Einträge vorhanden.</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="mb-3 page-size-selector">
|
||||
<span class="page-size-label">Datensätze je Seite:</span>
|
||||
<DxComboBox Data="@pageSizeOptions"
|
||||
TData="PageSizeOption"
|
||||
TValue="int?"
|
||||
TextFieldName="Text"
|
||||
ValueFieldName="Value"
|
||||
Value="@pageSize"
|
||||
ValueChanged="OnPageSizeChanged"
|
||||
CssClass="page-size-combo" />
|
||||
</div>
|
||||
|
||||
<div class="band-editor">
|
||||
<div class="band-controls">
|
||||
<DxButton Text="Band hinzufügen" Click="AddBand" />
|
||||
<DxButton Text="Layout speichern" Click="SaveLayoutAsync" Enabled="@CanSaveBandLayout" />
|
||||
<DxButton Text="Band-Layout zurücksetzen" Click="ResetBandLayoutAsync" />
|
||||
</div>
|
||||
@foreach (var band in bandLayout.Bands)
|
||||
{
|
||||
<div class="band-row">
|
||||
<DxTextBox Text="@band.Caption" TextChanged="@(value => UpdateBandCaption(band, value))" />
|
||||
<DxButton Text="Entfernen" Click="@(() => RemoveBand(band))" />
|
||||
</div>
|
||||
}
|
||||
<DxFormLayout CssClass="band-columns" ColCount="2">
|
||||
@foreach (var column in columnDefinitions)
|
||||
{
|
||||
<DxFormLayoutItem Caption="@column.Caption">
|
||||
<DxComboBox Data="@bandOptions"
|
||||
TData="BandOption"
|
||||
TValue="string"
|
||||
TextFieldName="Caption"
|
||||
ValueFieldName="Id"
|
||||
Value="@GetColumnBand(column.FieldName)"
|
||||
ValueChanged="@(value => UpdateColumnBand(column.FieldName, value))"
|
||||
Width="100%" />
|
||||
</DxFormLayoutItem>
|
||||
}
|
||||
</DxFormLayout>
|
||||
</div>
|
||||
|
||||
<div class="grid-section">
|
||||
<DxGrid Data="@items"
|
||||
TItem="MassDataReadDto"
|
||||
KeyFieldName="@nameof(MassDataReadDto.Id)"
|
||||
ShowGroupPanel="true"
|
||||
ShowGroupedColumns="true"
|
||||
AllowGroup="true"
|
||||
FilterMenuButtonDisplayMode="GridFilterMenuButtonDisplayMode.Always"
|
||||
AllowColumnResize="true"
|
||||
ColumnResizeMode="GridColumnResizeMode.ColumnsContainer"
|
||||
AllowColumnReorder="true"
|
||||
PagerVisible="false"
|
||||
PageSize="@(pageSize ?? 100)"
|
||||
CssClass="mb-3 massdata-grid"
|
||||
EditMode="GridEditMode.PopupEditForm"
|
||||
PopupEditFormHeaderText="@popupHeaderText"
|
||||
CustomizeEditModel="OnCustomizeEditModel"
|
||||
EditModelSaving="OnEditModelSaving"
|
||||
DataItemDeleting="OnDataItemDeleting"
|
||||
@ref="gridRef">
|
||||
<Columns>
|
||||
@RenderColumns()
|
||||
</Columns>
|
||||
<EditFormTemplate Context="editFormContext">
|
||||
@{ SetEditContext(editFormContext.EditContext); var editModel = (MassDataEditModel)editFormContext.EditModel; SetPopupHeaderText(editModel.IsNew); }
|
||||
<DxFormLayout ColCount="2">
|
||||
<DxFormLayoutItem Caption="CustomerName">
|
||||
<DxTextBox @bind-Text="editModel.CustomerName" Width="100%" />
|
||||
</DxFormLayoutItem>
|
||||
<DxFormLayoutItem Caption="Amount">
|
||||
<DxTextBox @bind-Text="editModel.AmountText" Width="100%" />
|
||||
</DxFormLayoutItem>
|
||||
<DxFormLayoutItem Caption="Category">
|
||||
<DxTextBox @bind-Text="editModel.Category" Width="100%" ReadOnly="@(!editModel.IsNew)" />
|
||||
</DxFormLayoutItem>
|
||||
<DxFormLayoutItem Caption="Status">
|
||||
<DxCheckBox @bind-Checked="editModel.StatusFlag" ReadOnly="@(!editModel.IsNew)" />
|
||||
</DxFormLayoutItem>
|
||||
@if (!editModel.IsNew)
|
||||
{
|
||||
<DxFormLayoutItem Caption="Prozedur">
|
||||
<DxComboBox Data="@procedureOptions"
|
||||
TData="ProcedureOption"
|
||||
TValue="int"
|
||||
TextFieldName="Text"
|
||||
ValueFieldName="Value"
|
||||
@bind-Value="editModel.UpdateProcedure"
|
||||
Width="100%" />
|
||||
</DxFormLayoutItem>
|
||||
}
|
||||
<DxFormLayoutItem ColSpanMd="12">
|
||||
<ValidationSummary />
|
||||
</DxFormLayoutItem>
|
||||
</DxFormLayout>
|
||||
</EditFormTemplate>
|
||||
</DxGrid>
|
||||
|
||||
@if (pageCount > 1)
|
||||
{
|
||||
<div class="pager-container">
|
||||
<DxPager PageCount="@pageCount" ActivePageIndex="@pageIndex" ActivePageIndexChanged="OnPageChanged" />
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
private List<MassDataReadDto> items = new();
|
||||
private bool isLoading;
|
||||
private bool hasLoaded;
|
||||
private string? errorMessage;
|
||||
private string? infoMessage;
|
||||
private int pageIndex;
|
||||
private int pageCount = 1;
|
||||
private int? pageSize = 100;
|
||||
private string popupHeaderText = "Edit";
|
||||
private EditContext? editContext;
|
||||
private ValidationMessageStore? validationMessageStore;
|
||||
private IGrid? gridRef;
|
||||
private const string LayoutType = "GRID_BANDS";
|
||||
private const string LayoutKey = "MassDataGrid";
|
||||
private const string LayoutUserStorageKey = "layoutUser";
|
||||
private string? layoutUser;
|
||||
private BandLayout bandLayout = new();
|
||||
private Dictionary<string, string> columnBandAssignments = new();
|
||||
private List<BandOption> bandOptions = new();
|
||||
private Dictionary<string, ColumnDefinition> columnLookup = new();
|
||||
private readonly JsonSerializerOptions jsonOptions = new(JsonSerializerDefaults.Web);
|
||||
private List<ColumnDefinition> columnDefinitions = new()
|
||||
{
|
||||
new() { FieldName = nameof(MassDataReadDto.Id), Caption = "Id", Width = "90px", ReadOnly = true, FilterType = ColumnFilterType.Text },
|
||||
new() { FieldName = nameof(MassDataReadDto.CustomerName), Caption = "CustomerName", FilterType = ColumnFilterType.Text },
|
||||
new() { FieldName = nameof(MassDataReadDto.Amount), Caption = "Amount", DisplayFormat = "c2", FilterType = ColumnFilterType.Text },
|
||||
new() { FieldName = nameof(MassDataReadDto.Category), Caption = "Category", ReadOnly = true, FilterType = ColumnFilterType.Text },
|
||||
new() { FieldName = nameof(MassDataReadDto.StatusFlag), Caption = "Status", ReadOnly = true, FilterType = ColumnFilterType.Bool },
|
||||
new() { FieldName = nameof(MassDataReadDto.AddedWhen), Caption = "Angelegt am", ReadOnly = true, FilterType = ColumnFilterType.Date },
|
||||
new() { FieldName = nameof(MassDataReadDto.ChangedWhen), Caption = "Geändert am", ReadOnly = true, FilterType = ColumnFilterType.Date }
|
||||
};
|
||||
|
||||
private bool CanSaveBandLayout => !string.IsNullOrWhiteSpace(layoutUser);
|
||||
|
||||
private readonly List<PageSizeOption> pageSizeOptions = new()
|
||||
{
|
||||
new() { Value = 100, Text = "100" },
|
||||
new() { Value = 1000, Text = "1.000" },
|
||||
new() { Value = 10000, Text = "10.000" },
|
||||
new() { Value = 100000, Text = "100.000" },
|
||||
new() { Value = null, Text = "Alle" }
|
||||
};
|
||||
|
||||
private readonly List<BoolFilterOption> statusFilterOptions = new()
|
||||
{
|
||||
new() { Value = null, Text = "Alle" },
|
||||
new() { Value = true, Text = "True" },
|
||||
new() { Value = false, Text = "False" }
|
||||
};
|
||||
|
||||
private readonly List<ProcedureOption> procedureOptions = new()
|
||||
{
|
||||
new() { Value = 0, Text = "PRMassdata_UpsertByCustomerName" }
|
||||
};
|
||||
|
||||
private bool gridLayoutApplied;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
columnLookup = columnDefinitions.ToDictionary(column => column.FieldName, StringComparer.OrdinalIgnoreCase);
|
||||
await EnsureLayoutUserAsync();
|
||||
await LoadBandLayoutAsync();
|
||||
await LoadPage(0);
|
||||
}
|
||||
|
||||
private async Task LoadPage(int page)
|
||||
{
|
||||
isLoading = true;
|
||||
errorMessage = null;
|
||||
try
|
||||
{
|
||||
var total = await Api.GetCountAsync();
|
||||
var effectivePageSize = pageSize ?? (total == 0 ? 1 : total);
|
||||
pageCount = Math.Max(1, (int)Math.Ceiling(total / (double)effectivePageSize));
|
||||
pageIndex = Math.Clamp(page, 0, pageCount - 1);
|
||||
|
||||
var skip = pageSize.HasValue ? pageIndex * pageSize.Value : 0;
|
||||
items = await Api.GetAllAsync(skip, pageSize);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errorMessage = $"MassData konnten nicht geladen werden: {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
isLoading = false;
|
||||
hasLoaded = true;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnPageChanged(int index)
|
||||
{
|
||||
await LoadPage(index);
|
||||
}
|
||||
|
||||
private async Task OnPageSizeChanged(int? size)
|
||||
{
|
||||
pageSize = size;
|
||||
await LoadPage(0);
|
||||
}
|
||||
|
||||
private async Task EnsureLayoutUserAsync()
|
||||
{
|
||||
layoutUser = await JsRuntime.InvokeAsync<string?>("localStorage.getItem", LayoutUserStorageKey);
|
||||
if (string.IsNullOrWhiteSpace(layoutUser))
|
||||
{
|
||||
layoutUser = Guid.NewGuid().ToString("N");
|
||||
await JsRuntime.InvokeVoidAsync("localStorage.setItem", LayoutUserStorageKey, layoutUser);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadBandLayoutAsync()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(layoutUser))
|
||||
{
|
||||
bandLayout = new BandLayout();
|
||||
UpdateBandOptions();
|
||||
return;
|
||||
}
|
||||
|
||||
var stored = await LayoutApi.GetAsync(LayoutType, LayoutKey, layoutUser);
|
||||
if (stored != null && !string.IsNullOrWhiteSpace(stored.LayoutData))
|
||||
{
|
||||
var parsed = JsonSerializer.Deserialize<BandLayout>(stored.LayoutData, jsonOptions);
|
||||
bandLayout = NormalizeBandLayout(parsed);
|
||||
}
|
||||
else
|
||||
{
|
||||
bandLayout = new BandLayout();
|
||||
}
|
||||
|
||||
columnBandAssignments = BuildAssignmentsFromLayout(bandLayout);
|
||||
ApplyColumnLayoutFromStorage();
|
||||
UpdateBandOptions();
|
||||
}
|
||||
|
||||
private async Task SaveLayoutAsync()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(layoutUser))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
CaptureColumnLayoutFromGrid();
|
||||
|
||||
var layoutData = JsonSerializer.Serialize(bandLayout, jsonOptions);
|
||||
await LayoutApi.UpsertAsync(new LayoutDto
|
||||
{
|
||||
LayoutType = LayoutType,
|
||||
LayoutKey = LayoutKey,
|
||||
UserName = layoutUser,
|
||||
LayoutData = layoutData
|
||||
});
|
||||
infoMessage = "Layout gespeichert.";
|
||||
errorMessage = null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errorMessage = $"Layout konnte nicht gespeichert werden: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
private void CaptureColumnLayoutFromGrid()
|
||||
{
|
||||
if (gridRef == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var layout = gridRef.SaveLayout();
|
||||
bandLayout.GridLayout = layout;
|
||||
|
||||
var orderedColumns = layout.Columns
|
||||
.Where(column => !string.IsNullOrWhiteSpace(column.FieldName))
|
||||
.OrderBy(column => column.VisibleIndex)
|
||||
.ToList();
|
||||
|
||||
bandLayout.ColumnOrder = orderedColumns
|
||||
.Select(column => column.FieldName)
|
||||
.ToList();
|
||||
|
||||
bandLayout.ColumnWidths = orderedColumns
|
||||
.Where(column => !string.IsNullOrWhiteSpace(column.Width))
|
||||
.ToDictionary(column => column.FieldName, column => column.Width, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private async Task ResetBandLayoutAsync()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(layoutUser))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await LayoutApi.DeleteAsync(LayoutType, LayoutKey, layoutUser);
|
||||
bandLayout = new BandLayout();
|
||||
columnBandAssignments.Clear();
|
||||
UpdateBandOptions();
|
||||
infoMessage = "Band-Layout zurückgesetzt.";
|
||||
}
|
||||
|
||||
private void ApplyColumnLayoutFromStorage()
|
||||
{
|
||||
foreach (var column in columnDefinitions)
|
||||
{
|
||||
if (bandLayout.ColumnWidths.TryGetValue(column.FieldName, out var width) && !string.IsNullOrWhiteSpace(width))
|
||||
{
|
||||
column.Width = width;
|
||||
}
|
||||
}
|
||||
|
||||
columnLookup = columnDefinitions.ToDictionary(column => column.FieldName, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private void AddBand()
|
||||
{
|
||||
bandLayout.Bands.Add(new BandDefinition
|
||||
{
|
||||
Id = Guid.NewGuid().ToString("N"),
|
||||
Caption = "Band"
|
||||
});
|
||||
UpdateBandOptions();
|
||||
}
|
||||
|
||||
private void RemoveBand(BandDefinition band)
|
||||
{
|
||||
bandLayout.Bands.Remove(band);
|
||||
var removedColumns = columnBandAssignments.Where(pair => pair.Value == band.Id)
|
||||
.Select(pair => pair.Key)
|
||||
.ToList();
|
||||
foreach (var column in removedColumns)
|
||||
{
|
||||
columnBandAssignments.Remove(column);
|
||||
}
|
||||
UpdateBandOptions();
|
||||
SyncBandsFromAssignments();
|
||||
}
|
||||
|
||||
private void UpdateBandCaption(BandDefinition band, string value)
|
||||
{
|
||||
band.Caption = value;
|
||||
UpdateBandOptions();
|
||||
}
|
||||
|
||||
private void UpdateColumnBand(string fieldName, string? bandId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(bandId))
|
||||
{
|
||||
columnBandAssignments.Remove(fieldName);
|
||||
}
|
||||
else
|
||||
{
|
||||
columnBandAssignments[fieldName] = bandId;
|
||||
}
|
||||
|
||||
SyncBandsFromAssignments();
|
||||
}
|
||||
|
||||
private string GetColumnBand(string fieldName)
|
||||
{
|
||||
return columnBandAssignments.TryGetValue(fieldName, out var bandId) ? bandId : string.Empty;
|
||||
}
|
||||
|
||||
private void SyncBandsFromAssignments()
|
||||
{
|
||||
foreach (var band in bandLayout.Bands)
|
||||
{
|
||||
band.Columns = columnDefinitions
|
||||
.Where(column => columnBandAssignments.TryGetValue(column.FieldName, out var bandId) && bandId == band.Id)
|
||||
.Select(column => column.FieldName)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private void UpdateBandOptions()
|
||||
{
|
||||
bandOptions = new List<BandOption> { new() { Id = string.Empty, Caption = "Ohne Band" } };
|
||||
bandOptions.AddRange(bandLayout.Bands.Select(band => new BandOption { Id = band.Id, Caption = band.Caption }));
|
||||
}
|
||||
|
||||
private Dictionary<string, string> BuildAssignmentsFromLayout(BandLayout layout)
|
||||
{
|
||||
var assignments = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var band in layout.Bands)
|
||||
{
|
||||
foreach (var column in band.Columns)
|
||||
{
|
||||
assignments[column] = band.Id;
|
||||
}
|
||||
}
|
||||
|
||||
return assignments;
|
||||
}
|
||||
|
||||
private BandLayout NormalizeBandLayout(BandLayout? layout)
|
||||
{
|
||||
layout ??= new BandLayout();
|
||||
layout.Bands ??= new List<BandDefinition>();
|
||||
layout.ColumnOrder ??= new List<string>();
|
||||
layout.ColumnWidths ??= new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var band in layout.Bands)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(band.Id))
|
||||
{
|
||||
band.Id = Guid.NewGuid().ToString("N");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(band.Caption))
|
||||
{
|
||||
band.Caption = "Band";
|
||||
}
|
||||
|
||||
band.Columns = band.Columns?.Where(columnLookup.ContainsKey).ToList() ?? new List<string>();
|
||||
}
|
||||
|
||||
return layout;
|
||||
}
|
||||
|
||||
private RenderFragment RenderColumns() => builder =>
|
||||
{
|
||||
var seq = 0;
|
||||
builder.OpenComponent<DxGridCommandColumn>(seq++);
|
||||
builder.AddAttribute(seq++, "Width", "120px");
|
||||
builder.CloseComponent();
|
||||
|
||||
var grouped = bandLayout.Bands.SelectMany(band => band.Columns).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var column in columnDefinitions.Where(column => !grouped.Contains(column.FieldName)))
|
||||
{
|
||||
BuildDataColumn(builder, ref seq, column);
|
||||
}
|
||||
|
||||
foreach (var band in bandLayout.Bands)
|
||||
{
|
||||
if (band.Columns.Count == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
builder.OpenComponent<DxGridBandColumn>(seq++);
|
||||
builder.AddAttribute(seq++, "Caption", band.Caption);
|
||||
builder.AddAttribute(seq++, "Columns", (RenderFragment)(bandBuilder =>
|
||||
{
|
||||
var bandSeq = 0;
|
||||
foreach (var columnName in band.Columns)
|
||||
{
|
||||
if (columnLookup.TryGetValue(columnName, out var column))
|
||||
{
|
||||
BuildDataColumn(bandBuilder, ref bandSeq, column);
|
||||
}
|
||||
}
|
||||
}));
|
||||
builder.CloseComponent();
|
||||
}
|
||||
};
|
||||
|
||||
private void BuildDataColumn(RenderTreeBuilder builder, ref int seq, ColumnDefinition column)
|
||||
{
|
||||
builder.OpenComponent<DxGridDataColumn>(seq++);
|
||||
builder.AddAttribute(seq++, "FieldName", column.FieldName);
|
||||
builder.AddAttribute(seq++, "Caption", column.Caption);
|
||||
if (!string.IsNullOrWhiteSpace(column.Width))
|
||||
{
|
||||
builder.AddAttribute(seq++, "Width", column.Width);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(column.DisplayFormat))
|
||||
{
|
||||
builder.AddAttribute(seq++, "DisplayFormat", column.DisplayFormat);
|
||||
}
|
||||
|
||||
if (column.ReadOnly)
|
||||
{
|
||||
builder.AddAttribute(seq++, "ReadOnly", true);
|
||||
}
|
||||
|
||||
builder.CloseComponent();
|
||||
}
|
||||
|
||||
private void SetEditContext(EditContext context)
|
||||
{
|
||||
if (editContext == context)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (editContext != null)
|
||||
{
|
||||
editContext.OnFieldChanged -= OnEditFieldChanged;
|
||||
}
|
||||
|
||||
editContext = context;
|
||||
validationMessageStore = new ValidationMessageStore(editContext);
|
||||
editContext.OnFieldChanged += OnEditFieldChanged;
|
||||
}
|
||||
|
||||
private void OnEditFieldChanged(object? sender, FieldChangedEventArgs e)
|
||||
{
|
||||
if (validationMessageStore == null || editContext == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.FieldIdentifier.FieldName == nameof(MassDataEditModel.UpdateProcedure))
|
||||
{
|
||||
validationMessageStore.Clear();
|
||||
editContext.NotifyValidationStateChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.FieldIdentifier.FieldName == nameof(MassDataEditModel.CustomerName))
|
||||
{
|
||||
var field = new FieldIdentifier(editContext.Model, nameof(MassDataEditModel.CustomerName));
|
||||
validationMessageStore.Clear(field);
|
||||
editContext.NotifyValidationStateChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private void SetPopupHeaderText(bool isNew)
|
||||
{
|
||||
popupHeaderText = isNew ? "Neu" : "Edit";
|
||||
}
|
||||
|
||||
private async Task OnCustomizeEditModel(GridCustomizeEditModelEventArgs e)
|
||||
{
|
||||
if (e.IsNew)
|
||||
{
|
||||
e.EditModel = new MassDataEditModel { IsNew = true, UpdateProcedure = procedureOptions[0].Value };
|
||||
SetPopupHeaderText(true);
|
||||
return;
|
||||
}
|
||||
|
||||
var item = (MassDataReadDto)e.DataItem;
|
||||
e.EditModel = new MassDataEditModel
|
||||
{
|
||||
Id = item.Id,
|
||||
CustomerName = item.CustomerName,
|
||||
AmountText = item.Amount.ToString("0.00"),
|
||||
Category = item.Category,
|
||||
StatusFlag = item.StatusFlag,
|
||||
UpdateProcedure = procedureOptions[0].Value,
|
||||
IsNew = false,
|
||||
OriginalCustomerName = item.CustomerName
|
||||
};
|
||||
SetPopupHeaderText(false);
|
||||
}
|
||||
|
||||
private async Task OnEditModelSaving(GridEditModelSavingEventArgs e)
|
||||
{
|
||||
errorMessage = null;
|
||||
infoMessage = null;
|
||||
|
||||
validationMessageStore?.Clear();
|
||||
editContext?.NotifyValidationStateChanged();
|
||||
|
||||
var editModel = (MassDataEditModel)e.EditModel;
|
||||
if (!decimal.TryParse(editModel.AmountText, out var amount))
|
||||
{
|
||||
AddValidationError(editModel, nameof(MassDataEditModel.AmountText), "Amount ist ungültig.");
|
||||
e.Cancel = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (editModel.IsNew)
|
||||
{
|
||||
var existing = await Api.GetByCustomerNameAsync(editModel.CustomerName);
|
||||
if (existing != null)
|
||||
{
|
||||
AddValidationError(editModel, nameof(MassDataEditModel.CustomerName), "Kunde existiert bereits.");
|
||||
e.Cancel = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var dto = new MassDataWriteDto
|
||||
{
|
||||
CustomerName = editModel.CustomerName,
|
||||
Amount = amount,
|
||||
Category = editModel.Category,
|
||||
StatusFlag = editModel.StatusFlag
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
await Api.UpsertAsync(dto);
|
||||
infoMessage = editModel.IsNew ? "MassData angelegt." : "MassData aktualisiert.";
|
||||
await LoadPage(pageIndex);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errorMessage = $"Fehler beim Speichern: {ex.Message}";
|
||||
e.Cancel = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void AddValidationError(MassDataEditModel editModel, string fieldName, string message)
|
||||
{
|
||||
if (editContext == null || validationMessageStore == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var field = new FieldIdentifier(editModel, fieldName);
|
||||
validationMessageStore.Add(field, message);
|
||||
editContext.NotifyValidationStateChanged();
|
||||
}
|
||||
|
||||
private Task OnDataItemDeleting(GridDataItemDeletingEventArgs e)
|
||||
{
|
||||
errorMessage = null;
|
||||
infoMessage = "Löschen ist aktuell noch nicht verfügbar.";
|
||||
e.Cancel = true;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private sealed class BandLayout
|
||||
{
|
||||
public List<BandDefinition> Bands { get; set; } = new();
|
||||
public List<string> ColumnOrder { get; set; } = new();
|
||||
public Dictionary<string, string?> ColumnWidths { get; set; } = new(StringComparer.OrdinalIgnoreCase);
|
||||
public GridPersistentLayout? GridLayout { get; set; }
|
||||
}
|
||||
|
||||
private sealed class BandDefinition
|
||||
{
|
||||
public string Id { get; set; } = string.Empty;
|
||||
public string Caption { get; set; } = string.Empty;
|
||||
public List<string> Columns { get; set; } = new();
|
||||
}
|
||||
|
||||
private sealed class BandOption
|
||||
{
|
||||
public string Id { get; set; } = string.Empty;
|
||||
public string Caption { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
private sealed class ColumnDefinition
|
||||
{
|
||||
public string FieldName { get; init; } = string.Empty;
|
||||
public string Caption { get; init; } = string.Empty;
|
||||
public string? Width { get; set; }
|
||||
public string? DisplayFormat { get; init; }
|
||||
public bool ReadOnly { get; init; }
|
||||
public ColumnFilterType FilterType { get; init; }
|
||||
}
|
||||
|
||||
private enum ColumnFilterType
|
||||
{
|
||||
Text,
|
||||
Bool,
|
||||
Date
|
||||
}
|
||||
|
||||
private sealed class MassDataEditModel
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string CustomerName { get; set; } = string.Empty;
|
||||
public string AmountText { get; set; } = string.Empty;
|
||||
public string Category { get; set; } = string.Empty;
|
||||
public bool StatusFlag { get; set; }
|
||||
public int UpdateProcedure { get; set; }
|
||||
public bool IsNew { get; set; }
|
||||
public string OriginalCustomerName { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
private sealed class ProcedureOption
|
||||
{
|
||||
public int Value { get; set; }
|
||||
public string Text { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
private sealed class BoolFilterOption
|
||||
{
|
||||
public bool? Value { get; set; }
|
||||
public string Text { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
private sealed class PageSizeOption
|
||||
{
|
||||
public int? Value { get; set; }
|
||||
public string Text { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
private sealed class FilterOperatorOption
|
||||
{
|
||||
public string Value { get; set; } = string.Empty;
|
||||
public string Text { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (!gridLayoutApplied && gridRef != null && bandLayout.GridLayout != null)
|
||||
{
|
||||
gridRef.LoadLayout(bandLayout.GridLayout);
|
||||
gridLayoutApplied = true;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="DevExpress.Blazor" Version="25.2.3" />
|
||||
<PackageReference Include="DevExpress.Blazor.Dashboard" Version="25.2.3" />
|
||||
<PackageReference Include="DevExpress.Blazor.Themes" Version="25.2.3" />
|
||||
<PackageReference Include="DevExpress.Blazor.Themes.Fluent" Version="25.2.3" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.22" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.22" PrivateAssets="all" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="8.0.22" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="wwwroot\sample-data\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,19 +0,0 @@
|
||||
@* Definiert das Hauptlayout der Anwendung.
|
||||
Enthält die Navigationsleiste und den Hauptinhalt. *@
|
||||
|
||||
@inherits LayoutComponentBase
|
||||
<div class="page">
|
||||
<div class="sidebar">
|
||||
<NavMenu /> <!-- Einbindung der Navigationsleiste -->
|
||||
</div>
|
||||
|
||||
<main>
|
||||
<div class="top-row px-4">
|
||||
<a href="https://learn.microsoft.com/aspnet/core/" target="_blank">About</a>
|
||||
</div>
|
||||
|
||||
<article class="content px-4">
|
||||
@Body <!-- Platzhalter für den Seiteninhalt -->
|
||||
</article>
|
||||
</main>
|
||||
</div>
|
||||
@@ -1,77 +0,0 @@
|
||||
.page {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
main {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
|
||||
}
|
||||
|
||||
.top-row {
|
||||
background-color: #f7f7f7;
|
||||
border-bottom: 1px solid #d6d5d5;
|
||||
justify-content: flex-end;
|
||||
height: 3.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.top-row ::deep a, .top-row ::deep .btn-link {
|
||||
white-space: nowrap;
|
||||
margin-left: 1.5rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.top-row ::deep a:first-child {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
@media (max-width: 640.98px) {
|
||||
.top-row {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.top-row ::deep a, .top-row ::deep .btn-link {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 641px) {
|
||||
.page {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 250px;
|
||||
height: 100vh;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.top-row {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.top-row.auth ::deep a:first-child {
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.top-row, article {
|
||||
padding-left: 2rem !important;
|
||||
padding-right: 1.5rem !important;
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
@* Definiert die Navigationsleiste, die Links zu verschiedenen Seiten der Anwendung enthält. *@
|
||||
|
||||
<div class="top-row ps-3 navbar navbar-dark">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="">DbFirst.BlazorWasm</a>
|
||||
<button title="Navigation menu" class="navbar-toggler" @onclick="ToggleNavMenu">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="@NavMenuCssClass nav-scrollable" @onclick="ToggleNavMenu">
|
||||
<nav class="flex-column">
|
||||
<div class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
|
||||
<span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home
|
||||
</NavLink>
|
||||
</div>
|
||||
<div class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="catalogs">
|
||||
<span class="bi bi-collection-nav-menu" aria-hidden="true"></span> Catalogs
|
||||
</NavLink>
|
||||
</div>
|
||||
<div class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="dashboards">
|
||||
<span class="bi bi-speedometer-nav-menu" aria-hidden="true"></span> Dashboards
|
||||
</NavLink>
|
||||
</div>
|
||||
<div class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="massdata">
|
||||
<span class="bi bi-table-nav-menu" aria-hidden="true"></span> MassData
|
||||
</NavLink>
|
||||
</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;
|
||||
}
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
.navbar-toggler {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.top-row {
|
||||
height: 3.5rem;
|
||||
background-color: rgba(0,0,0,0.4);
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.bi {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
margin-right: 0.75rem;
|
||||
top: -1px;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.bi-house-door-fill-nav-menu {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.bi-plus-square-fill-nav-menu {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.bi-list-nested-nav-menu {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.bi-collection-nav-menu {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' viewBox='0 0 16 16'%3E%3Cpath d='M2 3a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 .5.5v1H2V3z'/%3E%3Cpath d='M2 5h12v8a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.bi-speedometer-nav-menu {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' viewBox='0 0 16 16'%3E%3Cpath d='M1 11a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1V7a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v4zm5 0a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H7a1 1 0 0 0-1 1v8zm5 0a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1V9a1 1 0 0 0-1-1h-2a1 1 0 0 0-1 1v2z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.bi-table-nav-menu {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' viewBox='0 0 16 16'%3E%3Cpath d='M1 2a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V2zm1 1v2h12V3H2zm12 3H2v2h12V6zm0 3H2v2h12V9zm0 3H2v1h12v-1z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
font-size: 0.9rem;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.nav-item:first-of-type {
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.nav-item:last-of-type {
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.nav-item ::deep a {
|
||||
color: #d7d7d7;
|
||||
border-radius: 4px;
|
||||
height: 3rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
line-height: 3rem;
|
||||
}
|
||||
|
||||
.nav-item ::deep a.active {
|
||||
background-color: rgba(255,255,255,0.37);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nav-item ::deep a:hover {
|
||||
background-color: rgba(255,255,255,0.1);
|
||||
color: white;
|
||||
}
|
||||
|
||||
@media (min-width: 641px) {
|
||||
.navbar-toggler {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.collapse {
|
||||
/* Never collapse the sidebar for wide screens */
|
||||
display: block;
|
||||
}
|
||||
|
||||
.nav-scrollable {
|
||||
/* Allow sidebar to scroll for tall menus */
|
||||
height: calc(100vh - 3.5rem);
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
namespace DbFirst.BlazorWasm.Models;
|
||||
|
||||
public class CatalogReadDto
|
||||
{
|
||||
public int Guid { get; set; }
|
||||
public string CatTitle { get; set; } = string.Empty;
|
||||
public string CatString { get; set; } = string.Empty;
|
||||
public string AddedWho { get; set; } = string.Empty;
|
||||
public DateTime AddedWhen { get; set; }
|
||||
public string? ChangedWho { get; set; }
|
||||
public DateTime? ChangedWhen { get; set; }
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
namespace DbFirst.BlazorWasm.Models;
|
||||
|
||||
public class CatalogWriteDto
|
||||
{
|
||||
public string CatTitle { get; set; } = string.Empty;
|
||||
public string CatString { get; set; } = string.Empty;
|
||||
public int UpdateProcedure { get; set; } = 0; // 0 = Update, 1 = Save
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
namespace DbFirst.BlazorWasm.Models;
|
||||
|
||||
public class DashboardInfoDto
|
||||
{
|
||||
public string Id { get; set; } = string.Empty;
|
||||
public string Name { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
namespace DbFirst.BlazorWasm.Models;
|
||||
|
||||
public class LayoutDto
|
||||
{
|
||||
public string LayoutType { get; set; } = string.Empty;
|
||||
public string LayoutKey { get; set; } = string.Empty;
|
||||
public string UserName { get; set; } = string.Empty;
|
||||
public string LayoutData { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
namespace DbFirst.BlazorWasm.Models;
|
||||
|
||||
public class MassDataReadDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string CustomerName { get; set; } = string.Empty;
|
||||
public decimal Amount { get; set; }
|
||||
public string Category { get; set; } = string.Empty;
|
||||
public bool StatusFlag { get; set; }
|
||||
public DateTime AddedWhen { get; set; }
|
||||
public DateTime? ChangedWhen { get; set; }
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
namespace DbFirst.BlazorWasm.Models;
|
||||
|
||||
public class MassDataWriteDto
|
||||
{
|
||||
public string CustomerName { get; set; } = string.Empty;
|
||||
public decimal Amount { get; set; }
|
||||
public string Category { get; set; } = string.Empty;
|
||||
public bool StatusFlag { get; set; }
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
@page "/catalogs"
|
||||
|
||||
<PageTitle>Catalogs</PageTitle>
|
||||
|
||||
<h1>Catalogs</h1>
|
||||
|
||||
<CatalogsGrid />
|
||||
@@ -1,163 +0,0 @@
|
||||
@page "/dashboard"
|
||||
@page "/dashboards/{DashboardId?}"
|
||||
@implements IAsyncDisposable
|
||||
@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}?mode={(IsDesigner ? "designer" : "viewer")}")">@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 HubConnection? _hubConnection;
|
||||
|
||||
private bool IsDesigner => !string.Equals(Mode, "viewer", StringComparison.OrdinalIgnoreCase);
|
||||
private WorkingMode CurrentMode => IsDesigner ? WorkingMode.Designer : WorkingMode.ViewerOnly;
|
||||
private string SelectedDashboardId { get; set; } = string.Empty;
|
||||
private string DashboardKey => $"{SelectedDashboardId}-{(IsDesigner ? "designer" : "viewer")}";
|
||||
|
||||
private string DashboardEndpoint => $"{Configuration["ApiBaseUrl"]?.TrimEnd('/')}/api/dashboard";
|
||||
private string HubEndpoint => $"{Configuration["ApiBaseUrl"]?.TrimEnd('/')}/hubs/dashboards";
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await RefreshDashboards();
|
||||
|
||||
_hubConnection = new HubConnectionBuilder()
|
||||
.WithUrl(HubEndpoint)
|
||||
.WithAutomaticReconnect()
|
||||
.Build();
|
||||
|
||||
_hubConnection.On("DashboardsChanged", async () =>
|
||||
{
|
||||
await RefreshDashboards();
|
||||
});
|
||||
|
||||
await _hubConnection.StartAsync();
|
||||
}
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
if (dashboards.Count == 0)
|
||||
{
|
||||
await RefreshDashboards();
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
private async Task RefreshDashboards()
|
||||
{
|
||||
var latest = await DashboardApi.GetAllAsync();
|
||||
if (latest.Count == dashboards.Count && latest.All(d => dashboards.Any(x => x.Id == d.Id && x.Name == d.Name)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
dashboards.Clear();
|
||||
dashboards.AddRange(latest);
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (_hubConnection != null)
|
||||
{
|
||||
await _hubConnection.DisposeAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
@page "/"
|
||||
|
||||
<PageTitle>Home</PageTitle>
|
||||
|
||||
<h1>Db First approach</h1>
|
||||
|
||||
This is a Blazor WebAssembly application demonstrating the Database First approach using DevExpress components.
|
||||
@@ -1,7 +0,0 @@
|
||||
@page "/massdata"
|
||||
|
||||
<PageTitle>MassData</PageTitle>
|
||||
|
||||
<h1>MassData</h1>
|
||||
|
||||
<MassDataGrid />
|
||||
@@ -1,24 +0,0 @@
|
||||
/* Initialisiert die Blazor WebAssembly-Anwendung.
|
||||
Registriert Root-Komponenten
|
||||
Konfiguriert Abhängigkeiten */
|
||||
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||
using DbFirst.BlazorWasm;
|
||||
using DbFirst.BlazorWasm.Services;
|
||||
using DevExpress.Blazor;
|
||||
|
||||
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
||||
builder.RootComponents.Add<App>("#app");
|
||||
builder.RootComponents.Add<HeadOutlet>("head::after");
|
||||
|
||||
builder.Services.AddDevExpressBlazor();
|
||||
|
||||
var apiBaseUrl = builder.Configuration["ApiBaseUrl"] ?? builder.HostEnvironment.BaseAddress;
|
||||
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(apiBaseUrl) });
|
||||
builder.Services.AddScoped<CatalogApiClient>();
|
||||
builder.Services.AddScoped<DashboardApiClient>();
|
||||
builder.Services.AddScoped<MassDataApiClient>();
|
||||
builder.Services.AddScoped<LayoutApiClient>();
|
||||
|
||||
await builder.Build().RunAsync();
|
||||
@@ -1,41 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json.schemastore.org/launchsettings.json",
|
||||
"iisSettings": {
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:12804",
|
||||
"sslPort": 44394
|
||||
}
|
||||
},
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
|
||||
"applicationUrl": "http://localhost:5101",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
|
||||
"applicationUrl": "https://localhost:7276;http://localhost:5101",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"IIS Express": {
|
||||
"commandName": "IISExpress",
|
||||
"launchBrowser": true,
|
||||
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
/* Kapselt die Kommunikation mit der API für den Catalog-Endpunkt.
|
||||
Bietet Methoden für CRUD-Operationen auf Catalog-Daten */
|
||||
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using DbFirst.BlazorWasm.Models;
|
||||
|
||||
namespace DbFirst.BlazorWasm.Services;
|
||||
|
||||
public class CatalogApiClient
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private const string Endpoint = "api/catalogs";
|
||||
|
||||
public CatalogApiClient(HttpClient httpClient)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
}
|
||||
|
||||
public async Task<List<CatalogReadDto>> GetAllAsync()
|
||||
{
|
||||
var result = await _httpClient.GetFromJsonAsync<List<CatalogReadDto>>(Endpoint);
|
||||
return result ?? new List<CatalogReadDto>();
|
||||
}
|
||||
|
||||
public async Task<CatalogReadDto?> GetByIdAsync(int id)
|
||||
{
|
||||
return await _httpClient.GetFromJsonAsync<CatalogReadDto>($"{Endpoint}/{id}");
|
||||
}
|
||||
|
||||
public async Task<ApiResult<CatalogReadDto?>> CreateAsync(CatalogWriteDto dto)
|
||||
{
|
||||
var response = await _httpClient.PostAsJsonAsync(Endpoint, dto);
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
var payload = await response.Content.ReadFromJsonAsync<CatalogReadDto>();
|
||||
return ApiResult<CatalogReadDto?>.Ok(payload);
|
||||
}
|
||||
|
||||
var error = await ReadErrorAsync(response);
|
||||
return ApiResult<CatalogReadDto?>.Fail(error);
|
||||
}
|
||||
|
||||
public async Task<ApiResult<bool>> UpdateAsync(int id, CatalogWriteDto dto)
|
||||
{
|
||||
var response = await _httpClient.PutAsJsonAsync($"{Endpoint}/{id}", dto);
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
return ApiResult<bool>.Ok(true);
|
||||
}
|
||||
|
||||
var error = await ReadErrorAsync(response);
|
||||
return ApiResult<bool>.Fail(error);
|
||||
}
|
||||
|
||||
public async Task<ApiResult<bool>> DeleteAsync(int id)
|
||||
{
|
||||
var response = await _httpClient.DeleteAsync($"{Endpoint}/{id}");
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
return ApiResult<bool>.Ok(true);
|
||||
}
|
||||
|
||||
var error = await ReadErrorAsync(response);
|
||||
return ApiResult<bool>.Fail(error);
|
||||
}
|
||||
|
||||
private static async Task<string> ReadErrorAsync(HttpResponseMessage response)
|
||||
{
|
||||
// Liest und analysiert Fehlerdetails aus der API-Antwort.
|
||||
// Gibt eine benutzerfreundliche Fehlermeldung zurück.
|
||||
|
||||
string? problemTitle = null;
|
||||
string? problemDetail = null;
|
||||
|
||||
try
|
||||
{
|
||||
var problem = await response.Content.ReadFromJsonAsync<ProblemDetailsDto>();
|
||||
if (problem != null)
|
||||
{
|
||||
problemTitle = problem.Title;
|
||||
problemDetail = problem.Detail ?? problem.Type;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore parse errors
|
||||
}
|
||||
|
||||
var status = response.StatusCode;
|
||||
var reason = response.ReasonPhrase;
|
||||
var body = await response.Content.ReadAsStringAsync();
|
||||
|
||||
string detail = problemDetail;
|
||||
if (string.IsNullOrWhiteSpace(detail) && !string.IsNullOrWhiteSpace(body))
|
||||
{
|
||||
detail = body;
|
||||
}
|
||||
|
||||
// Friendly overrides
|
||||
if (status == HttpStatusCode.Conflict)
|
||||
{
|
||||
return "Datensatz existiert bereits. Bitte wählen Sie einen anderen Titel.";
|
||||
}
|
||||
if (status == HttpStatusCode.BadRequest && (detail?.Contains("CatTitle cannot be changed", StringComparison.OrdinalIgnoreCase) ?? false))
|
||||
{
|
||||
return "Titel kann nicht geändert werden.";
|
||||
}
|
||||
|
||||
return status switch
|
||||
{
|
||||
HttpStatusCode.BadRequest => $"Eingabe ungültig{FormatSuffix(problemTitle, detail, reason)}",
|
||||
HttpStatusCode.NotFound => $"Nicht gefunden{FormatSuffix(problemTitle, detail, reason)}",
|
||||
HttpStatusCode.Conflict => $"Konflikt{FormatSuffix(problemTitle, detail, reason)}",
|
||||
HttpStatusCode.Unauthorized => $"Nicht autorisiert{FormatSuffix(problemTitle, detail, reason)}",
|
||||
HttpStatusCode.Forbidden => $"Nicht erlaubt{FormatSuffix(problemTitle, detail, reason)}",
|
||||
HttpStatusCode.InternalServerError => $"Serverfehler{FormatSuffix(problemTitle, detail, reason)}",
|
||||
_ => $"Fehler {(int)status} {reason ?? string.Empty}{FormatSuffix(problemTitle, detail, reason)}"
|
||||
};
|
||||
}
|
||||
|
||||
private static string FormatSuffix(string? title, string? detail, string? reason)
|
||||
{
|
||||
// Formatiert zusätzliche Informationen für Fehlermeldungen.
|
||||
// Kombiniert Titel, Details und Grund in einer lesbaren Form.
|
||||
|
||||
var parts = new List<string>();
|
||||
if (!string.IsNullOrWhiteSpace(title)) parts.Add(title);
|
||||
if (!string.IsNullOrWhiteSpace(detail)) parts.Add(detail);
|
||||
if (parts.Count == 0 && !string.IsNullOrWhiteSpace(reason)) parts.Add(reason);
|
||||
if (parts.Count == 0) return string.Empty;
|
||||
return ": " + string.Join(" | ", parts);
|
||||
}
|
||||
}
|
||||
|
||||
public record ApiResult<T>(bool Success, T? Value, string? Error)
|
||||
{
|
||||
public static ApiResult<T> Ok(T? value) => new(true, value, null);
|
||||
public static ApiResult<T> Fail(string? error) => new(false, default, error);
|
||||
}
|
||||
|
||||
internal sealed class ProblemDetailsDto
|
||||
{
|
||||
public string? Type { get; set; }
|
||||
public string? Title { get; set; }
|
||||
public string? Detail { get; set; }
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
using System.Net.Http.Json;
|
||||
using DbFirst.BlazorWasm.Models;
|
||||
|
||||
namespace DbFirst.BlazorWasm.Services;
|
||||
|
||||
public class DashboardApiClient
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private const string Endpoint = "api/dashboard/dashboards";
|
||||
|
||||
public DashboardApiClient(HttpClient httpClient)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
}
|
||||
|
||||
public async Task<List<DashboardInfoDto>> GetAllAsync()
|
||||
{
|
||||
var result = await _httpClient.GetFromJsonAsync<List<DashboardInfoDto>>(Endpoint);
|
||||
return result ?? new List<DashboardInfoDto>();
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
using System.Net.Http.Json;
|
||||
using DbFirst.BlazorWasm.Models;
|
||||
|
||||
namespace DbFirst.BlazorWasm.Services;
|
||||
|
||||
public class LayoutApiClient
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private const string Endpoint = "api/layouts";
|
||||
|
||||
public LayoutApiClient(HttpClient httpClient)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
}
|
||||
|
||||
public async Task<LayoutDto?> GetAsync(string layoutType, string layoutKey, string userName)
|
||||
{
|
||||
var url = $"{Endpoint}?layoutType={Uri.EscapeDataString(layoutType)}&layoutKey={Uri.EscapeDataString(layoutKey)}&userName={Uri.EscapeDataString(userName)}";
|
||||
var response = await _httpClient.GetAsync(url);
|
||||
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
return await response.Content.ReadFromJsonAsync<LayoutDto>();
|
||||
}
|
||||
|
||||
public async Task<LayoutDto> UpsertAsync(LayoutDto dto)
|
||||
{
|
||||
var response = await _httpClient.PostAsJsonAsync(Endpoint, dto);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var detail = await ReadErrorAsync(response);
|
||||
throw new InvalidOperationException(detail);
|
||||
}
|
||||
|
||||
var payload = await response.Content.ReadFromJsonAsync<LayoutDto>();
|
||||
return payload ?? dto;
|
||||
}
|
||||
|
||||
private static async Task<string> ReadErrorAsync(HttpResponseMessage response)
|
||||
{
|
||||
var body = await response.Content.ReadAsStringAsync();
|
||||
if (!string.IsNullOrWhiteSpace(body))
|
||||
{
|
||||
return body;
|
||||
}
|
||||
|
||||
return $"{(int)response.StatusCode} {response.ReasonPhrase}".Trim();
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(string layoutType, string layoutKey, string userName)
|
||||
{
|
||||
var url = $"{Endpoint}?layoutType={Uri.EscapeDataString(layoutType)}&layoutKey={Uri.EscapeDataString(layoutKey)}&userName={Uri.EscapeDataString(userName)}";
|
||||
var response = await _httpClient.DeleteAsync(url);
|
||||
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
using System.Net.Http.Json;
|
||||
using DbFirst.BlazorWasm.Models;
|
||||
|
||||
namespace DbFirst.BlazorWasm.Services;
|
||||
|
||||
public class MassDataApiClient
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private const string Endpoint = "api/massdata";
|
||||
|
||||
public MassDataApiClient(HttpClient httpClient)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
}
|
||||
|
||||
public async Task<int> GetCountAsync()
|
||||
{
|
||||
var result = await _httpClient.GetFromJsonAsync<int?>("api/massdata/count");
|
||||
return result ?? 0;
|
||||
}
|
||||
|
||||
public async Task<List<MassDataReadDto>> GetAllAsync(int? skip, int? take)
|
||||
{
|
||||
var query = new List<string>();
|
||||
if (skip.HasValue)
|
||||
{
|
||||
query.Add($"skip={skip.Value}");
|
||||
}
|
||||
if (take.HasValue)
|
||||
{
|
||||
query.Add($"take={take.Value}");
|
||||
}
|
||||
|
||||
var url = query.Count == 0 ? Endpoint : $"{Endpoint}?{string.Join("&", query)}";
|
||||
var result = await _httpClient.GetFromJsonAsync<List<MassDataReadDto>>(url);
|
||||
return result ?? new List<MassDataReadDto>();
|
||||
}
|
||||
|
||||
public async Task<MassDataReadDto> UpsertAsync(MassDataWriteDto dto)
|
||||
{
|
||||
var response = await _httpClient.PostAsJsonAsync($"{Endpoint}/upsert", dto);
|
||||
response.EnsureSuccessStatusCode();
|
||||
var payload = await response.Content.ReadFromJsonAsync<MassDataReadDto>();
|
||||
return payload ?? new MassDataReadDto();
|
||||
}
|
||||
|
||||
public async Task<MassDataReadDto?> GetByCustomerNameAsync(string customerName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(customerName))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var response = await _httpClient.GetAsync($"{Endpoint}/{Uri.EscapeDataString(customerName)}");
|
||||
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
return await response.Content.ReadFromJsonAsync<MassDataReadDto>();
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
@using System.Net.Http
|
||||
@using System.Net.Http.Json
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Microsoft.AspNetCore.Components.Routing
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||
@using Microsoft.AspNetCore.Components.WebAssembly.Http
|
||||
@using Microsoft.JSInterop
|
||||
@using Microsoft.AspNetCore.SignalR.Client
|
||||
@using DbFirst.BlazorWasm
|
||||
@using DbFirst.BlazorWasm.Layout
|
||||
@using DbFirst.BlazorWasm.Models
|
||||
@using DbFirst.BlazorWasm.Services
|
||||
@using DbFirst.BlazorWasm.Components
|
||||
@using DevExpress.Blazor
|
||||
@using DevExpress.DashboardBlazor
|
||||
@using DevExpress.DashboardWeb
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"ApiBaseUrl": "https://localhost:7204/"
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
html, body {
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
h1:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
a, .btn-link {
|
||||
color: #0071c1;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
color: #fff;
|
||||
background-color: #1b6ec2;
|
||||
border-color: #1861ac;
|
||||
}
|
||||
|
||||
.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
|
||||
box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding-top: 1.1rem;
|
||||
}
|
||||
|
||||
.valid.modified:not([type=checkbox]) {
|
||||
outline: 1px solid #26b050;
|
||||
}
|
||||
|
||||
.invalid {
|
||||
outline: 1px solid red;
|
||||
}
|
||||
|
||||
.validation-message {
|
||||
color: red;
|
||||
}
|
||||
|
||||
#blazor-error-ui {
|
||||
background: lightyellow;
|
||||
bottom: 0;
|
||||
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
|
||||
display: none;
|
||||
left: 0;
|
||||
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
#blazor-error-ui .dismiss {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: 0.75rem;
|
||||
top: 0.5rem;
|
||||
}
|
||||
|
||||
.blazor-error-boundary {
|
||||
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
|
||||
padding: 1rem 1rem 1rem 3.7rem;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.blazor-error-boundary::after {
|
||||
content: "An error has occurred."
|
||||
}
|
||||
|
||||
.loading-progress {
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 8rem;
|
||||
height: 8rem;
|
||||
margin: 20vh auto 1rem auto;
|
||||
}
|
||||
|
||||
.loading-progress circle {
|
||||
fill: none;
|
||||
stroke: #e0e0e0;
|
||||
stroke-width: 0.6rem;
|
||||
transform-origin: 50% 50%;
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.loading-progress circle:last-child {
|
||||
stroke: #1b6ec2;
|
||||
stroke-dasharray: calc(3.141 * var(--blazor-load-percentage, 0%) * 0.8), 500%;
|
||||
transition: stroke-dasharray 0.05s ease-in-out;
|
||||
}
|
||||
|
||||
.loading-progress-text {
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
inset: calc(20vh + 3.25rem) 0 auto 0.2rem;
|
||||
}
|
||||
|
||||
.loading-progress-text:after {
|
||||
content: var(--blazor-load-percentage-text, "Loading");
|
||||
}
|
||||
|
||||
code {
|
||||
color: #c02d76;
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
|
Before Width: | Height: | Size: 1.1 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 2.6 KiB |
@@ -1,58 +0,0 @@
|
||||
<!--
|
||||
• Ist der technische Einstiegspunkt der Blazor WebAssembly-Anwendung.
|
||||
• Sie lädt die notwendigen Ressourcen (z. B. das Blazor-Skript blazor.webassembly.js)
|
||||
und definiert den Platzhalter <div id="app">, in dem die Blazor-Komponenten gerendert werden.
|
||||
• Ohne diese Datei könnte die Blazor-Anwendung nicht starten, da sie die Verbindung
|
||||
zwischen der statischen HTML-Welt und der Blazor-Welt herstellt.
|
||||
kurz: Startet die Anwendung und lädt die Blazor-Umgebung.
|
||||
-->
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>DbFirst.BlazorWasm</title>
|
||||
<base href="/" />
|
||||
|
||||
<!-- Stylesheets für DevExpress und Bootstrap -->
|
||||
|
||||
<link href="_content/DevExpress.Blazor.Dashboard/ace.css" rel="stylesheet" />
|
||||
<link href="_content/DevExpress.Blazor.Dashboard/ace-theme-dreamweaver.css" rel="stylesheet" />
|
||||
<link href="_content/DevExpress.Blazor.Dashboard/ace-theme-ambiance.css" rel="stylesheet" />
|
||||
<link href="_content/DevExpress.Blazor.Dashboard/dx.light.css" rel="stylesheet" />
|
||||
<link href="_content/DevExpress.Blazor.Dashboard/dx-analytics.common.css" rel="stylesheet" />
|
||||
<link href="_content/DevExpress.Blazor.Dashboard/dx-analytics.light.css" rel="stylesheet" />
|
||||
<link href="_content/DevExpress.Blazor.Dashboard/dx-querybuilder.css" rel="stylesheet" />
|
||||
<link href="_content/DevExpress.Blazor.Dashboard/dx-dashboard.light.min.css" rel="stylesheet" />
|
||||
|
||||
<link rel="stylesheet" href="css/bootstrap/bootstrap.min.css" />
|
||||
<link rel="stylesheet" href="css/app.css" />
|
||||
|
||||
<link rel="icon" type="image/png" href="favicon.png" />
|
||||
<link href="DbFirst.BlazorWasm.styles.css" rel="stylesheet" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- Einstiegspunkt der Blazor-Anwendung -->
|
||||
<div id="app">
|
||||
<svg class="loading-progress">
|
||||
<circle r="40%" cx="50%" cy="50%" />
|
||||
<circle r="40%" cx="50%" cy="50%" />
|
||||
</svg>
|
||||
<div class="loading-progress-text"></div>
|
||||
</div>
|
||||
|
||||
<!-- Fehler-UI für unvorhergesehene Fehler -->
|
||||
<div id="blazor-error-ui">
|
||||
An unhandled error has occurred.
|
||||
<a href="" class="reload">Reload</a>
|
||||
<a class="dismiss">🗙</a>
|
||||
</div>
|
||||
|
||||
<!-- Blazor WebAssembly-Skript -->
|
||||
<script src="_framework/blazor.webassembly.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
14
DbFirst.sln
14
DbFirst.sln
@@ -11,8 +11,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DbFirst.Infrastructure", "D
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DbFirst.Domain", "DbFirst.Domain\DbFirst.Domain.csproj", "{E989468B-CBF1-49F4-954E-4FFEE7CE5A77}"
|
||||
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
|
||||
@@ -73,18 +71,6 @@ Global
|
||||
{E989468B-CBF1-49F4-954E-4FFEE7CE5A77}.Release|x64.Build.0 = Release|Any CPU
|
||||
{E989468B-CBF1-49F4-954E-4FFEE7CE5A77}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{E989468B-CBF1-49F4-954E-4FFEE7CE5A77}.Release|x86.Build.0 = Release|Any CPU
|
||||
{666BE786-6D04-4224-9948-FF13597481A0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{666BE786-6D04-4224-9948-FF13597481A0}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{666BE786-6D04-4224-9948-FF13597481A0}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{666BE786-6D04-4224-9948-FF13597481A0}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{666BE786-6D04-4224-9948-FF13597481A0}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{666BE786-6D04-4224-9948-FF13597481A0}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{666BE786-6D04-4224-9948-FF13597481A0}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{666BE786-6D04-4224-9948-FF13597481A0}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{666BE786-6D04-4224-9948-FF13597481A0}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{666BE786-6D04-4224-9948-FF13597481A0}.Release|x64.Build.0 = Release|Any CPU
|
||||
{666BE786-6D04-4224-9948-FF13597481A0}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{666BE786-6D04-4224-9948-FF13597481A0}.Release|x86.Build.0 = Release|Any CPU
|
||||
{FF1D77C4-A13D-43E0-BCE1-C18C01F1767C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{FF1D77C4-A13D-43E0-BCE1-C18C01F1767C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{FF1D77C4-A13D-43E0-BCE1-C18C01F1767C}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
|
||||
Reference in New Issue
Block a user