From 8933deec96dddfd7cf84d135ff36bc2096e5420f Mon Sep 17 00:00:00 2001 From: OlgunR Date: Tue, 5 May 2026 16:41:15 +0200 Subject: [PATCH] Add dark mode override for non-native DevExpress themes Implements a dark mode override system for DevExpress Blazor themes lacking native dark support. Adds a JS function to toggle a dx-dark class on , updates ThemeState to detect native dark themes, and applies targeted CSS variable overrides for consistent dark styling. Disables prerendering to ensure JS interop, and improves theme switching logic and documentation. --- DbFirst.BlazorWebApp/Components/App.razor | 8 ++ .../Components/Layout/MainLayout.razor | 41 +++++++- DbFirst.BlazorWebApp/Components/Routes.razor | 3 +- DbFirst.BlazorWebApp/Services/ThemeState.cs | 18 +++- .../appsettings.Development.json | 8 +- DbFirst.BlazorWebApp/wwwroot/app.css | 99 +++++++++++++++++++ .../wwwroot/js/size-manager.js | 1 + 7 files changed, 168 insertions(+), 10 deletions(-) diff --git a/DbFirst.BlazorWebApp/Components/App.razor b/DbFirst.BlazorWebApp/Components/App.razor index f6e8697..3a4ea34 100644 --- a/DbFirst.BlazorWebApp/Components/App.razor +++ b/DbFirst.BlazorWebApp/Components/App.razor @@ -24,6 +24,14 @@ + diff --git a/DbFirst.BlazorWebApp/Components/Layout/MainLayout.razor b/DbFirst.BlazorWebApp/Components/Layout/MainLayout.razor index 2f4e964..77cd534 100644 --- a/DbFirst.BlazorWebApp/Components/Layout/MainLayout.razor +++ b/DbFirst.BlazorWebApp/Components/Layout/MainLayout.razor @@ -1,8 +1,9 @@ @inherits LayoutComponentBase @implements IDisposable @inject ThemeState ThemeState +@inject IJSRuntime JS -
+
@@ -33,9 +34,43 @@
@code { + private bool _isInteractive; + protected override void OnInitialized() { - ThemeState.OnChange += StateHasChanged; + ThemeState.OnChange += OnThemeChanged; + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + _isInteractive = true; + } + await ApplyDxDarkOverrideAsync(); + } + + private async void OnThemeChanged() + { + StateHasChanged(); + if (_isInteractive) + { + await ApplyDxDarkOverrideAsync(); + } + } + + private async Task ApplyDxDarkOverrideAsync() + { + if (!_isInteractive) return; + try + { + bool needsOverride = ThemeState.IsDarkMode && !ThemeState.IsNativeDarkTheme; + await JS.InvokeVoidAsync("setDxDarkOverride", needsOverride); + } + catch (JSException) + { + // JS-Funktion noch nicht verfügbar – kein Circuit-Crash + } } private void ToggleTheme() @@ -45,6 +80,6 @@ public void Dispose() { - ThemeState.OnChange -= StateHasChanged; + ThemeState.OnChange -= OnThemeChanged; } } diff --git a/DbFirst.BlazorWebApp/Components/Routes.razor b/DbFirst.BlazorWebApp/Components/Routes.razor index f7f43eb..2daf851 100644 --- a/DbFirst.BlazorWebApp/Components/Routes.razor +++ b/DbFirst.BlazorWebApp/Components/Routes.razor @@ -1,4 +1,5 @@ -@rendermode InteractiveServer +@rendermode @(new InteractiveServerRenderMode(prerender: false)) + diff --git a/DbFirst.BlazorWebApp/Services/ThemeState.cs b/DbFirst.BlazorWebApp/Services/ThemeState.cs index 7b72b26..cd0eeb6 100644 --- a/DbFirst.BlazorWebApp/Services/ThemeState.cs +++ b/DbFirst.BlazorWebApp/Services/ThemeState.cs @@ -14,6 +14,16 @@ public class ThemeState public bool IsDarkMode { get; private set; } public string CurrentThemeName { get; private set; } = "Fluent"; + /// + /// Themes die eine native DevExpress Dark-Variante besitzen: + /// - Fluent ? Themes.Fluent.Clone(ThemeMode.Dark), verwendet --DS-* Token-System + /// - BlazingBerry ? Themes.BlazingDark + /// Alle anderen Themes (Purple, OfficeWhite, BootstrapExternal) haben keine offizielle + /// Dark-Variante; dort übernehmen CSS-Overrides auf --dxbl-grid-* Variablen die Arbeit. + /// + public bool IsNativeDarkTheme => IsDarkMode && + (CurrentThemeName == "Fluent" || CurrentThemeName == "BlazingBerry"); + public static readonly List AvailableThemes = ["Fluent", "BlazingBerry", "Purple", "OfficeWhite", "BootstrapExternal"]; public event Action? OnChange; @@ -45,13 +55,11 @@ public class ThemeState }); themeChangeService.SetTheme(theme); } - else if (CurrentThemeName == "BlazingBerry") themeChangeService.SetTheme(Themes.BlazingBerry); - else if (CurrentThemeName == "Purple") themeChangeService.SetTheme(Themes.Purple); - else if (CurrentThemeName == "OfficeWhite") themeChangeService.SetTheme(Themes.OfficeWhite); + else if (CurrentThemeName == "BlazingBerry") themeChangeService.SetTheme(IsDarkMode ? Themes.BlazingDark : Themes.BlazingBerry); + else if (CurrentThemeName == "Purple") themeChangeService.SetTheme(Themes.Purple); + else if (CurrentThemeName == "OfficeWhite") themeChangeService.SetTheme(Themes.OfficeWhite); else if (CurrentThemeName == "BootstrapExternal") themeChangeService.SetTheme(Themes.BootstrapExternal); else themeChangeService.SetTheme(Themes.Fluent); - - OnChange?.Invoke(); } } \ No newline at end of file diff --git a/DbFirst.BlazorWebApp/appsettings.Development.json b/DbFirst.BlazorWebApp/appsettings.Development.json index 702a28b..be563e8 100644 --- a/DbFirst.BlazorWebApp/appsettings.Development.json +++ b/DbFirst.BlazorWebApp/appsettings.Development.json @@ -5,5 +5,11 @@ "Microsoft.AspNetCore": "Warning" } }, - "ApiBaseUrl": "https://localhost:7204/" + "ApiBaseUrl": "https://localhost:7204/", + "BrowserLink": { + "Enabled": false + }, + "DetailedErrors": true } + + diff --git a/DbFirst.BlazorWebApp/wwwroot/app.css b/DbFirst.BlazorWebApp/wwwroot/app.css index a0da4df..37c7c6d 100644 --- a/DbFirst.BlazorWebApp/wwwroot/app.css +++ b/DbFirst.BlazorWebApp/wwwroot/app.css @@ -148,6 +148,105 @@ dxbl-grid tbody tr:nth-child(even) td { background-color: var(--grid-stripe-bg) !important; } +/* ?? Dark-Mode-Overrides für nicht-native Themes ????????????????????????????? + Strategie: CSS-Custom-Properties werden von DevExpress DIREKT auf den + Komponenten-Elementen definiert (z. B. --dxbl-popup-bg:#fff auf .dxbl-modal). + Eine geerbte Variable aus html.dx-dark würde durch die direkte Zuweisung + überschrieben. Deshalb targeten wir exakt dieselben Elemente, aber mit einem + zusätzlichen Vorfahren-Selektor (html.dx-dark) für höhere Spezifizität: + html.dx-dark .dxbl-modal = (0,2,1) > .dxbl-modal = (0,1,0) ? + html.dx-dark wird per JS gesetzt, wenn IsDarkMode && !IsNativeDarkTheme. +?? */ + +/* Popup / Modal (CRUD-Dialoge) – Variablen-Quelle: .dxbl-modal */ +html.dx-dark .dxbl-modal { + --dxbl-popup-bg: #2d2d2d; + --dxbl-popup-color: #e8e8e8; + --dxbl-popup-border-color: #555; + --dxbl-popup-header-bg: #333; + --dxbl-popup-header-color: #e8e8e8; + --dxbl-popup-footer-bg: #333; + --dxbl-popup-footer-color: #e8e8e8; +} + +/* Flyout (Column Chooser, Filter-Panel) – Variablen-Quelle: .dxbl-flyout */ +html.dx-dark .dxbl-flyout { + --dxbl-flyout-bg: #2d2d2d; + --dxbl-flyout-color: #e8e8e8; + --dxbl-flyout-border-color: #555; + --dxbl-flyout-header-bg: #333; + --dxbl-flyout-header-color: #e8e8e8; + --dxbl-flyout-footer-bg: #333; +} + +/* Dropdown (ComboBox-Klappliste, Band-Dropdowns) – Quelle: .dxbl-dropdown */ +html.dx-dark .dxbl-dropdown, +html.dx-dark .dxbl-itemlist-dropdown { + --dxbl-dropdown-bg: #2d2d2d; + --dxbl-dropdown-color: #e8e8e8; + --dxbl-dropdown-border-color: #555; + --dxbl-dropdown-header-bg: #333; + --dxbl-dropdown-footer-bg: #333; +} + +/* Edit-Dropdown (ComboBox-Popup wenn als Modal gerendert) – Quelle: .dxbl-edit-dropdown */ +html.dx-dark .dxbl-edit-dropdown { + --dxbl-edit-dropdown-bg: #2d2d2d; + --dxbl-edit-dropdown-color: #e8e8e8; + --dxbl-edit-dropdown-border-color: #555; +} + +/* ListBox (Einträge in Dropdowns) – Quelle: .dxbl-list-box */ +html.dx-dark .dxbl-list-box, +html.dx-dark .dxbl-list-box-render-container { + --dxbl-list-box-bg: #2d2d2d; + --dxbl-list-box-color: #e8e8e8; + --dxbl-list-box-border-color: #555; + --dxbl-list-box-item-hover-bg: #3a3a3a; + --dxbl-list-box-item-hover-color: #e8e8e8; +} + +/* TextEdit / ComboBox – Eingabefeld – Quelle: .dxbl-text-edit */ +html.dx-dark .dxbl-text-edit { + --dxbl-text-edit-bg: #2d2d2d; + --dxbl-text-edit-color: #e8e8e8; + --dxbl-text-edit-border-color: #555; + --dxbl-text-edit-btn-bg: #3a3a3a; + --dxbl-text-edit-btn-color: #e8e8e8; + --dxbl-text-edit-btn-hover-bg: #444; + --dxbl-text-edit-btn-hover-color: #e8e8e8; +} + +/* Buttons */ +html.dx-dark .dxbl-btn { + --dxbl-btn-color: #e8e8e8; + --dxbl-btn-bg: #3a3a3a; + --dxbl-btn-border-color: #555; + --dxbl-btn-hover-bg: #444; + --dxbl-btn-hover-color: #e8e8e8; + --dxbl-btn-hover-border-color: #666; +} + +/* FormLayout */ +html.dx-dark .dxbl-fl { + --dxbl-fl-caption-color: #bbb; + --dxbl-fl-group-bg: #242424; + --dxbl-fl-group-color: #e8e8e8; +} + +/* Grid */ +html.dx-dark .dxbl-grid { + background-color: #242424; + color: #e8e8e8; + border-color: #444; +} + +html.dx-dark .dxbl-grid > .dxbl-scroll-viewer, +html.dx-dark .dxbl-grid > .dxbl-grid-top-panel { + background-color: #242424; + color: #e8e8e8; +} + /* MassData-spezifisch */ .page-size-selector { display: flex; diff --git a/DbFirst.BlazorWebApp/wwwroot/js/size-manager.js b/DbFirst.BlazorWebApp/wwwroot/js/size-manager.js index 3cd47e6..64d9a78 100644 --- a/DbFirst.BlazorWebApp/wwwroot/js/size-manager.js +++ b/DbFirst.BlazorWebApp/wwwroot/js/size-manager.js @@ -1,3 +1,4 @@ window.setSize = function (fontSize) { document.documentElement.style.setProperty('--global-size', fontSize); }; +