From f0fce36f1f1b1ac5c1134eabe32d72021d008a3e Mon Sep 17 00:00:00 2001 From: Yoojun Zhou Date: Sun, 26 Jul 2026 14:08:05 +0800 Subject: [PATCH 1/3] fix: stop DependencyPropertyDescriptor from keeping windows alive (#438) --- .../SettingsCard/SettingsCard.cs | 14 +-- .../Extended/MessageBox/MessageBox.cs | 12 +- .../Controls/Windows/InfoBar/InfoBar.cs | 12 +- .../Foundation/Design/TypographyPage.xaml.cs | 19 ++- .../Controls/AnimatedIcon.cs | 33 ++++- .../Controls/Helpers/WindowHelper.cs | 113 +++++++++++------- .../Controls/Primitives/TitleBarControl.cs | 38 ++++-- 7 files changed, 170 insertions(+), 71 deletions(-) diff --git a/source/iNKORE.UI.WPF.Modern.Controls/Controls/Community/SettingsControls/SettingsCard/SettingsCard.cs b/source/iNKORE.UI.WPF.Modern.Controls/Controls/Community/SettingsControls/SettingsCard/SettingsCard.cs index 12a963af..5f0d2010 100644 --- a/source/iNKORE.UI.WPF.Modern.Controls/Controls/Community/SettingsControls/SettingsCard/SettingsCard.cs +++ b/source/iNKORE.UI.WPF.Modern.Controls/Controls/Community/SettingsControls/SettingsCard/SettingsCard.cs @@ -51,21 +51,21 @@ static SettingsCard() DefaultStyleKeyProperty.OverrideMetadata(typeof(SettingsCard), new FrameworkPropertyMetadata(typeof(SettingsCard))); } - internal static readonly DependencyPropertyDescriptor IsPressedPropertyDescriptior = DependencyPropertyDescriptor.FromProperty(IsPressedProperty, typeof(SettingsCard)); - internal static readonly DependencyPropertyDescriptor IsMouseOverPropertyDescriptior = DependencyPropertyDescriptor.FromProperty(IsMouseOverProperty, typeof(SettingsCard)); - /// /// Creates a new instance of the class. /// public SettingsCard() { - IsPressedPropertyDescriptior.AddValueChanged(this, PointerStateProperties_ValueChanged); - IsMouseOverPropertyDescriptior.AddValueChanged(this, PointerStateProperties_ValueChanged); } - private void PointerStateProperties_ValueChanged(object sender, EventArgs e) + protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e) { - this.UpdatePointerState(); + base.OnPropertyChanged(e); + + if (e.Property == IsPressedProperty || e.Property == IsMouseOverProperty) + { + this.UpdatePointerState(); + } } /// diff --git a/source/iNKORE.UI.WPF.Modern.Controls/Controls/Extended/MessageBox/MessageBox.cs b/source/iNKORE.UI.WPF.Modern.Controls/Controls/Extended/MessageBox/MessageBox.cs index fc83386c..1aec6cf0 100644 --- a/source/iNKORE.UI.WPF.Modern.Controls/Controls/Extended/MessageBox/MessageBox.cs +++ b/source/iNKORE.UI.WPF.Modern.Controls/Controls/Extended/MessageBox/MessageBox.cs @@ -48,8 +48,6 @@ static MessageBox() DefaultStyleKeyProperty.OverrideMetadata(typeof(MessageBox), new FrameworkPropertyMetadata(typeof(MessageBox))); } - public static readonly DependencyPropertyDescriptor SystemBackdropTypeProperty_Descriptor = DependencyPropertyDescriptor.FromProperty(WindowHelper.SystemBackdropTypeProperty, typeof(MessageBox)); - public MessageBox() { CommandBindings.Add(new CommandBinding(ApplicationCommands.Copy, new ExecutedRoutedEventHandler(ExecuteCopy))); @@ -60,9 +58,17 @@ public MessageBox() Loaded += On_Loaded; - SystemBackdropTypeProperty_Descriptor.AddValueChanged(this, SystemBackdropTypeProperty_ValueChanged); ThemeManager.AddActualThemeChangedHandler(this, ThemeManager_AddActualThemeChanged); } + protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e) + { + base.OnPropertyChanged(e); + + if (e.Property == WindowHelper.SystemBackdropTypeProperty) + { + SystemBackdropTypeProperty_ValueChanged(this, EventArgs.Empty); + } + } private void ExecuteCopy(object sender, ExecutedRoutedEventArgs e) { diff --git a/source/iNKORE.UI.WPF.Modern.Controls/Controls/Windows/InfoBar/InfoBar.cs b/source/iNKORE.UI.WPF.Modern.Controls/Controls/Windows/InfoBar/InfoBar.cs index 4756765a..33b4f511 100644 --- a/source/iNKORE.UI.WPF.Modern.Controls/Controls/Windows/InfoBar/InfoBar.cs +++ b/source/iNKORE.UI.WPF.Modern.Controls/Controls/Windows/InfoBar/InfoBar.cs @@ -45,8 +45,16 @@ static InfoBar() public InfoBar() { SetValue(TemplateSettingsPropertyKey, new InfoBarTemplateSettings()); - DependencyPropertyDescriptor descriptor = DependencyPropertyDescriptor.FromProperty(ForegroundProperty, typeof(InfoBar)); - descriptor.AddValueChanged(this, (sender, e) => UpdateForeground()); + } + + protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e) + { + base.OnPropertyChanged(e); + + if (e.Property == ForegroundProperty) + { + UpdateForeground(); + } } protected override AutomationPeer OnCreateAutomationPeer() diff --git a/source/iNKORE.UI.WPF.Modern.Gallery/Pages/Controls/Foundation/Design/TypographyPage.xaml.cs b/source/iNKORE.UI.WPF.Modern.Gallery/Pages/Controls/Foundation/Design/TypographyPage.xaml.cs index 301faedd..a52adde6 100644 --- a/source/iNKORE.UI.WPF.Modern.Gallery/Pages/Controls/Foundation/Design/TypographyPage.xaml.cs +++ b/source/iNKORE.UI.WPF.Modern.Gallery/Pages/Controls/Foundation/Design/TypographyPage.xaml.cs @@ -29,11 +29,8 @@ public TypographyPage() ThemeManager.Current.ActualApplicationThemeChanged += OnThemeChanged; ThemeManager.AddActualThemeChangedHandler(this, OnElementThemeChanged); - - DependencyPropertyDescriptor.FromProperty(ThemeManager.RequestedThemeProperty, typeof(FrameworkElement)) - ?.AddValueChanged(this, OnRequestedThemeChanged); - - _themeMonitorTimer = new DispatcherTimer + + _themeMonitorTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(200) }; @@ -41,6 +38,18 @@ public TypographyPage() _themeMonitorTimer.Start(); } + // ThemeManager.RequestedTheme is watched by overriding OnPropertyChanged rather than through + // DependencyPropertyDescriptor.AddValueChanged, which would have kept this page alive forever. + protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e) + { + base.OnPropertyChanged(e); + + if (e.Property == ThemeManager.RequestedThemeProperty) + { + OnRequestedThemeChanged(this, EventArgs.Empty); + } + } + private void TypographyPage_Loaded(object sender, RoutedEventArgs e) { if (NavigationRootPage.Current?.NavigationView != null) diff --git a/source/iNKORE.UI.WPF.Modern/Controls/AnimatedIcon.cs b/source/iNKORE.UI.WPF.Modern/Controls/AnimatedIcon.cs index def25b21..752ddfc2 100644 --- a/source/iNKORE.UI.WPF.Modern/Controls/AnimatedIcon.cs +++ b/source/iNKORE.UI.WPF.Modern/Controls/AnimatedIcon.cs @@ -24,6 +24,7 @@ public class AnimatedIcon : IconElement public AnimatedIcon() { Loaded += OnLoaded; + Unloaded += OnUnloaded; } #region FallbackIconSource @@ -229,6 +230,9 @@ private void OnStatePropertyChanged() } } + private DependencyObject _ancestorWithState; + private EventHandler _ancestorStateChangedHandler; + private void OnLoaded(object UnnamedParameter, RoutedEventArgs UnnamedParameter2) { // AnimatedIcon might get added to a UI which has already set the State property on an ancestor. @@ -259,10 +263,16 @@ private void OnLoaded(object UnnamedParameter, RoutedEventArgs UnnamedParameter2 SetValue(property, stateValue); } - if (ancestorWithState != null) + DetachFromAncestorWithState(); + + if (ancestorWithState is DependencyObject ancestor) { - DependencyPropertyDescriptor descriptor = DependencyPropertyDescriptor.FromProperty(property, typeof(AnimatedIcon)); - descriptor.AddValueChanged(ancestorWithState, (sender, e) => OnAncestorAnimatedIconStatePropertyChanged(ancestorWithState, property)); + // The descriptor is cached for the lifetime of the process and keeps a strong reference to the + // ancestor, so the handler has to be removed again when this icon is unloaded + _ancestorWithState = ancestor; + _ancestorStateChangedHandler = (sender, e) => OnAncestorAnimatedIconStatePropertyChanged(ancestor, property); + DependencyPropertyDescriptor.FromProperty(property, typeof(AnimatedIcon)) + .AddValueChanged(ancestor, _ancestorStateChangedHandler); } // Wait until loaded to apply the fallback icon source property because we need the icon source @@ -271,6 +281,23 @@ private void OnLoaded(object UnnamedParameter, RoutedEventArgs UnnamedParameter2 OnFallbackIconSourcePropertyChanged(new DependencyPropertyChangedEventArgs()); } + private void OnUnloaded(object sender, RoutedEventArgs e) + { + DetachFromAncestorWithState(); + } + + private void DetachFromAncestorWithState() + { + if (_ancestorWithState != null) + { + DependencyPropertyDescriptor.FromProperty(StateProperty, typeof(AnimatedIcon)) + .RemoveValueChanged(_ancestorWithState, _ancestorStateChangedHandler); + + _ancestorWithState = null; + _ancestorStateChangedHandler = null; + } + } + private void OnAncestorAnimatedIconStatePropertyChanged(object sender, DependencyProperty args) { SetValue(StateProperty, ((DependencyObject)sender).GetValue(args)); diff --git a/source/iNKORE.UI.WPF.Modern/Controls/Helpers/WindowHelper.cs b/source/iNKORE.UI.WPF.Modern/Controls/Helpers/WindowHelper.cs index d78b5911..5dc7c86d 100644 --- a/source/iNKORE.UI.WPF.Modern/Controls/Helpers/WindowHelper.cs +++ b/source/iNKORE.UI.WPF.Modern/Controls/Helpers/WindowHelper.cs @@ -422,64 +422,35 @@ public static void SetWindowStyle(Window window) //bool isSetAcrylic10 = false; //bool isSetAero = false; - void ApplyDarkMode() - { - var theme = ThemeManager.GetActualTheme(window); - - bool IsDark(ElementTheme theme) - { - return theme == ElementTheme.Default - ? ThemeManager.Current.ActualApplicationTheme == ApplicationTheme.Dark - : theme == ElementTheme.Dark; - } - - try - { - if (IsDark(theme)) - { - window.ApplyDarkMode(); - } - else - { - window.RemoveDarkMode(); - } - } - catch { } - } - - var handler = new RoutedEventHandler((sender, e) => ApplyDarkMode()); - + // These handlers are static methods on purpose: the delegates created from them compare equal + // across calls, so the -= below actually removes what a previous call added instead of + // silently piling up another subscription. WindowResizeModeDescriptor.RemoveValueChanged(window, OnWindowResizeModeDescriptorValueChanged); - ThemeManager.RemoveActualThemeChangedHandler(window, handler); + ThemeManager.RemoveActualThemeChangedHandler(window, OnModernWindowActualThemeChanged); + window.Closed -= OnModernWindowClosed; if (isModern) { - ApplyDarkMode(); - - void onLoaded(object sender, RoutedEventArgs e) - { - // This is needed to fix the issue with the window not being loaded correctly - WindowChrome.SetWindowChrome(window, (WindowChrome.GetWindowChrome(window)?.Clone() as WindowChrome) ?? WindowChrome.GetWindowChrome(window)); - - window.RemoveTitleBar(); - } - + UpdateDarkMode(window); if (window.IsLoaded) { - onLoaded(null, null); + OnModernWindowLoaded(window, null); } else { - - window.Loaded -= onLoaded; - window.Loaded += onLoaded; + window.Loaded -= OnModernWindowLoaded; + window.Loaded += OnModernWindowLoaded; } - ThemeManager.AddActualThemeChangedHandler(window, handler); + ThemeManager.AddActualThemeChangedHandler(window, OnModernWindowActualThemeChanged); WindowResizeModeDescriptor.AddValueChanged(window, OnWindowResizeModeDescriptorValueChanged); + // WindowResizeModeDescriptor keeps a strong reference to every window given to AddValueChanged + // for the lifetime of the process, so it has to be released once the window is gone. + window.Closed += OnModernWindowClosed; + window.SetResourceReference(FrameworkElement.StyleProperty, TheWindowStyleKey); //if (isUseMica) @@ -536,6 +507,62 @@ void onLoaded(object sender, RoutedEventArgs e) UpdateShouldDisplayManualBorder(window); } + private static void UpdateDarkMode(Window window) + { + var theme = ThemeManager.GetActualTheme(window); + + bool IsDark(ElementTheme value) + { + return value == ElementTheme.Default + ? ThemeManager.Current.ActualApplicationTheme == ApplicationTheme.Dark + : value == ElementTheme.Dark; + } + + try + { + if (IsDark(theme)) + { + window.ApplyDarkMode(); + } + else + { + window.RemoveDarkMode(); + } + } + catch { } + } + + private static void OnModernWindowActualThemeChanged(object sender, RoutedEventArgs e) + { + if (sender is Window window) + { + UpdateDarkMode(window); + } + } + + private static void OnModernWindowLoaded(object sender, RoutedEventArgs e) + { + if (sender is Window window) + { + // This is needed to fix the issue with the window not being loaded correctly + WindowChrome.SetWindowChrome(window, (WindowChrome.GetWindowChrome(window)?.Clone() as WindowChrome) ?? WindowChrome.GetWindowChrome(window)); + + window.RemoveTitleBar(); + } + } + + private static void OnModernWindowClosed(object sender, EventArgs e) + { + if (sender is Window window) + { + window.Closed -= OnModernWindowClosed; + window.Loaded -= OnModernWindowLoaded; + + WindowResizeModeDescriptor.RemoveValueChanged(window, OnWindowResizeModeDescriptorValueChanged); + ThemeManager.RemoveActualThemeChangedHandler(window, OnModernWindowActualThemeChanged); + } + } + #region Chrome Management diff --git a/source/iNKORE.UI.WPF.Modern/Controls/Primitives/TitleBarControl.cs b/source/iNKORE.UI.WPF.Modern/Controls/Primitives/TitleBarControl.cs index 638b23ce..c04e9d46 100644 --- a/source/iNKORE.UI.WPF.Modern/Controls/Primitives/TitleBarControl.cs +++ b/source/iNKORE.UI.WPF.Modern/Controls/Primitives/TitleBarControl.cs @@ -522,14 +522,7 @@ protected override void OnVisualParentChanged(DependencyObject oldParent) { if (_parentWindow != null) { - descriptor_ResizeMode.RemoveValueChanged(_parentWindow, _window_ButtonAvailabilityShouldUpdate); - descriptor_WindowStyle.RemoveValueChanged(_parentWindow, _window_ButtonAvailabilityShouldUpdate); - - if (_altLeftBinding != null) - { - _parentWindow.InputBindings.Remove(_altLeftBinding); - _altLeftBinding = null; - } + DetachFromParentWindow(); } base.OnVisualParentChanged(oldParent); @@ -543,10 +536,35 @@ protected override void OnVisualParentChanged(DependencyObject oldParent) descriptor_ResizeMode.AddValueChanged(_parentWindow, _window_ButtonAvailabilityShouldUpdate); descriptor_WindowStyle.AddValueChanged(_parentWindow, _window_ButtonAvailabilityShouldUpdate); + // The descriptors above are cached for the lifetime of the process and keep a strong reference + // to the window, so they have to be released when it closes + _parentWindow.Closed += _window_Closed; + UpdateButtonActualAvailabilities(); } } + private void DetachFromParentWindow() + { + descriptor_ResizeMode.RemoveValueChanged(_parentWindow, _window_ButtonAvailabilityShouldUpdate); + descriptor_WindowStyle.RemoveValueChanged(_parentWindow, _window_ButtonAvailabilityShouldUpdate); + _parentWindow.Closed -= _window_Closed; + + if (_altLeftBinding != null) + { + _parentWindow.InputBindings.Remove(_altLeftBinding); + _altLeftBinding = null; + } + } + + private void _window_Closed(object sender, EventArgs e) + { + if (sender == _parentWindow) + { + DetachFromParentWindow(); + } + } + private void _window_ButtonAvailabilityShouldUpdate(object sender, EventArgs e) { if(sender == _parentWindow) @@ -576,6 +594,10 @@ private void OnBackButtonClick(object sender, RoutedEventArgs e) public void UpdateButtonActualAvailabilities() { + if (_parentWindow == null) + { + return; + } // Close button if (CloseButtonAvailability != TitleBarButtonAvailability.Auto) From 9cb6a9ca139b9a031beb593b08c7d4584b43d0f6 Mon Sep 17 00:00:00 2001 From: Yoojun Zhou Date: Sun, 26 Jul 2026 14:16:33 +0800 Subject: [PATCH 2/3] fix: (gallery/TypographyPage) release theme watchers on unload --- .../Foundation/Design/TypographyPage.xaml.cs | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/source/iNKORE.UI.WPF.Modern.Gallery/Pages/Controls/Foundation/Design/TypographyPage.xaml.cs b/source/iNKORE.UI.WPF.Modern.Gallery/Pages/Controls/Foundation/Design/TypographyPage.xaml.cs index a52adde6..cd91f502 100644 --- a/source/iNKORE.UI.WPF.Modern.Gallery/Pages/Controls/Foundation/Design/TypographyPage.xaml.cs +++ b/source/iNKORE.UI.WPF.Modern.Gallery/Pages/Controls/Foundation/Design/TypographyPage.xaml.cs @@ -26,8 +26,8 @@ public TypographyPage() { this.InitializeComponent(); Loaded += TypographyPage_Loaded; - - ThemeManager.Current.ActualApplicationThemeChanged += OnThemeChanged; + Unloaded += TypographyPage_Unloaded; + ThemeManager.AddActualThemeChangedHandler(this, OnElementThemeChanged); _themeMonitorTimer = new DispatcherTimer @@ -35,7 +35,6 @@ public TypographyPage() Interval = TimeSpan.FromMilliseconds(200) }; _themeMonitorTimer.Tick += ThemeMonitorTimer_Tick; - _themeMonitorTimer.Start(); } // ThemeManager.RequestedTheme is watched by overriding OnPropertyChanged rather than through @@ -52,16 +51,28 @@ protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e) private void TypographyPage_Loaded(object sender, RoutedEventArgs e) { + // Both of these keep the page alive while they are active: the event belongs to a singleton, and a + // running DispatcherTimer is held by the dispatcher. They are only hooked up while loaded + ThemeManager.Current.ActualApplicationThemeChanged -= OnThemeChanged; + ThemeManager.Current.ActualApplicationThemeChanged += OnThemeChanged; + _themeMonitorTimer.Start(); + if (NavigationRootPage.Current?.NavigationView != null) { NavigationRootPage.Current.NavigationView.Header = "Typography"; } - + UpdateTypographyImage(); - + UpdateExampleCode(); } + private void TypographyPage_Unloaded(object sender, RoutedEventArgs e) + { + ThemeManager.Current.ActualApplicationThemeChanged -= OnThemeChanged; + _themeMonitorTimer.Stop(); + } + //FAILED TRIALS but keeping for reference - The image should switch when toggle theme clicked private void UpdateTypographyImage() { From 7d941f7e6b5d1557c9f8569aa54d1352372b2ea5 Mon Sep 17 00:00:00 2001 From: Yoojun Zhou Date: Sun, 26 Jul 2026 14:29:57 +0800 Subject: [PATCH 3/3] refactor(gallery/TypographyPage): watch the image's own ActualTheme The header image sits inside the ControlExample, and the toggle theme button sets RequestedTheme on the example content and its container rather than on the page, so none of the page level theme notifications fired and the light/dark image never swapped. The page worked around that with a 200ms DispatcherTimer comparing the page's theme against the ControlExample's on every tick, which also meant UpdateTypographyImage ran five times a second, re-decoding the PNG each time, for as long as a toggled theme stayed in effect. ThemeManager keeps ActualTheme in sync on every element of the tree, so a single ActualThemeChanged handler on the image itself covers the toggle and application level theme changes alike. It replaces the timer, the multi-level theme detection, the singleton subscription, the RequestedTheme property override and the deferred one-shot refresh. --- .../Foundation/Design/TypographyPage.xaml.cs | 175 +----------------- 1 file changed, 9 insertions(+), 166 deletions(-) diff --git a/source/iNKORE.UI.WPF.Modern.Gallery/Pages/Controls/Foundation/Design/TypographyPage.xaml.cs b/source/iNKORE.UI.WPF.Modern.Gallery/Pages/Controls/Foundation/Design/TypographyPage.xaml.cs index cd91f502..4c666e6e 100644 --- a/source/iNKORE.UI.WPF.Modern.Gallery/Pages/Controls/Foundation/Design/TypographyPage.xaml.cs +++ b/source/iNKORE.UI.WPF.Modern.Gallery/Pages/Controls/Foundation/Design/TypographyPage.xaml.cs @@ -3,12 +3,10 @@ using iNKORE.UI.WPF.Modern.Controls; using System; -using System.ComponentModel; using System.Diagnostics; using System.Windows; using System.Windows.Navigation; using System.Windows.Media.Imaging; -using System.Windows.Threading; using iNKORE.UI.WPF.Modern.Gallery.Helpers; using iNKORE.UI.WPF.Modern; @@ -19,44 +17,20 @@ namespace iNKORE.UI.WPF.Modern.Gallery.Pages.Controls.Foundation /// public sealed partial class TypographyPage : Page { - private DispatcherTimer _themeMonitorTimer; - private ElementTheme _lastKnownTheme = ElementTheme.Default; - public TypographyPage() { this.InitializeComponent(); Loaded += TypographyPage_Loaded; - Unloaded += TypographyPage_Unloaded; - - ThemeManager.AddActualThemeChangedHandler(this, OnElementThemeChanged); - _themeMonitorTimer = new DispatcherTimer - { - Interval = TimeSpan.FromMilliseconds(200) - }; - _themeMonitorTimer.Tick += ThemeMonitorTimer_Tick; - } - - // ThemeManager.RequestedTheme is watched by overriding OnPropertyChanged rather than through - // DependencyPropertyDescriptor.AddValueChanged, which would have kept this page alive forever. - protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e) - { - base.OnPropertyChanged(e); - - if (e.Property == ThemeManager.RequestedThemeProperty) - { - OnRequestedThemeChanged(this, EventArgs.Empty); - } + // The header image sits inside the ControlExample, whose theme the gallery's toggle theme button + // flips on its own, so watching the theme of the page is not enough. ActualTheme is kept in sync on + // every element of the tree, so watching the image itself catches the toggle and application level + // theme changes alike. No need to detach: the handler belongs to the page the image is part of + ThemeManager.AddActualThemeChangedHandler(TypographyHeaderImage, OnImageActualThemeChanged); } private void TypographyPage_Loaded(object sender, RoutedEventArgs e) { - // Both of these keep the page alive while they are active: the event belongs to a singleton, and a - // running DispatcherTimer is held by the dispatcher. They are only hooked up while loaded - ThemeManager.Current.ActualApplicationThemeChanged -= OnThemeChanged; - ThemeManager.Current.ActualApplicationThemeChanged += OnThemeChanged; - _themeMonitorTimer.Start(); - if (NavigationRootPage.Current?.NavigationView != null) { NavigationRootPage.Current.NavigationView.Header = "Typography"; @@ -67,67 +41,17 @@ private void TypographyPage_Loaded(object sender, RoutedEventArgs e) UpdateExampleCode(); } - private void TypographyPage_Unloaded(object sender, RoutedEventArgs e) + private void OnImageActualThemeChanged(object sender, RoutedEventArgs e) { - ThemeManager.Current.ActualApplicationThemeChanged -= OnThemeChanged; - _themeMonitorTimer.Stop(); + UpdateTypographyImage(); } - //FAILED TRIALS but keeping for reference - The image should switch when toggle theme clicked private void UpdateTypographyImage() { if (TypographyHeaderImage == null) return; - // Multi-level theme detection to catch both application and element-level changes - var pageTheme = ThemeManager.GetActualTheme(this); - var parentTheme = ElementTheme.Default; - var controlExampleTheme = ElementTheme.Default; - - // Check parent elements for theme overrides (catches toggle theme changes) - var parentElement = this.Parent as FrameworkElement; - while (parentElement != null) - { - var currentParentTheme = ThemeManager.GetActualTheme(parentElement); - if (currentParentTheme != ElementTheme.Default) - { - parentTheme = currentParentTheme; - break; - } - parentElement = parentElement.Parent as FrameworkElement; - } - - // Check ControlExample elements for theme changes (this is where toggle theme applies changes) - if (Example1 != null) - { - try - { - var exampleTheme = ThemeManager.GetActualTheme(Example1); - if (exampleTheme != ElementTheme.Default) - { - controlExampleTheme = exampleTheme; - } - else if (Example1.ExampleContainer != null) - { - var containerTheme = ThemeManager.GetActualTheme(Example1.ExampleContainer); - if (containerTheme != ElementTheme.Default) - { - controlExampleTheme = containerTheme; - } - } - } - catch (Exception ex) - { - // Handle potential issues with theme detection during control initialization - System.Diagnostics.Debug.WriteLine($"Theme detection error: {ex.Message}"); - } - } - - // Use the most specific theme available (ControlExample > Page > Parent > Application) - var effectiveTheme = controlExampleTheme != ElementTheme.Default ? controlExampleTheme : - pageTheme != ElementTheme.Default ? pageTheme : parentTheme; - var isDarkTheme = effectiveTheme == ElementTheme.Dark || - (effectiveTheme == ElementTheme.Default && ThemeHelper.IsDarkTheme()); - + var isDarkTheme = ThemeManager.GetActualTheme(TypographyHeaderImage) == ElementTheme.Dark; + var imageName = isDarkTheme ? "Typography.dark.png" : "Typography.light.png"; var uri = new System.Uri($"pack://application:,,,/iNKORE.UI.WPF.Modern.Gallery;component/Assets/Design/{imageName}"); @@ -142,8 +66,6 @@ private void UpdateTypographyImage() bitmapImage.EndInit(); bitmapImage.Freeze(); TypographyHeaderImage.Source = bitmapImage; - - System.Diagnostics.Debug.WriteLine($"Typography image updated to: {imageName} (Page: {pageTheme}, Parent: {parentTheme}, ControlExample: {controlExampleTheme}, Effective: {effectiveTheme}, IsDark: {isDarkTheme})"); } catch (Exception ex) { @@ -154,85 +76,6 @@ private void UpdateTypographyImage() } } - private void OnThemeChanged(ThemeManager sender, object args) - { - // Update the image when theme changes - UpdateTypographyImage(); - } - - private void OnElementThemeChanged(object sender, RoutedEventArgs e) - { - // Update the image when element theme changes (for theme toggle) - UpdateTypographyImage(); - } - - private void ThemeMonitorTimer_Tick(object sender, EventArgs e) - { - // Check if the theme has changed by monitoring our current actual theme - var currentTheme = ThemeManager.GetActualTheme(this); - if (currentTheme != _lastKnownTheme) - { - _lastKnownTheme = currentTheme; - UpdateTypographyImage(); - System.Diagnostics.Debug.WriteLine($"Theme change detected: {currentTheme}"); - } - - // Also check for element-level theme changes by examining parent elements - // This catches toggle theme changes that affect control examples - var parentElement = this.Parent as FrameworkElement; - while (parentElement != null) - { - var parentTheme = ThemeManager.GetActualTheme(parentElement); - if (parentTheme != currentTheme) - { - // Found a parent with different theme - this indicates element-level theme change - UpdateTypographyImage(); - System.Diagnostics.Debug.WriteLine($"Element-level theme change detected: Parent={parentTheme}, Current={currentTheme}"); - break; - } - parentElement = parentElement.Parent as FrameworkElement; - } - - if (Example1 != null) - { - try - { - var controlExampleTheme = ThemeManager.GetActualTheme(Example1); - var containerTheme = ElementTheme.Default; - - if (Example1.ExampleContainer != null) - { - containerTheme = ThemeManager.GetActualTheme(Example1.ExampleContainer); - } - - if (controlExampleTheme != currentTheme || containerTheme != currentTheme) - { - UpdateTypographyImage(); - System.Diagnostics.Debug.WriteLine($"ControlExample theme change detected: ControlExample={controlExampleTheme}, Container={containerTheme}, Page={currentTheme}"); - } - } - catch (Exception ex) - { - // Handle potential issues with theme detection during control initialization - System.Diagnostics.Debug.WriteLine($"Theme monitor error: {ex.Message}"); - } - } - } - - private void OnRequestedThemeChanged(object sender, EventArgs e) - { - Dispatcher.BeginInvoke(new System.Action(() => { - UpdateTypographyImage(); - }), System.Windows.Threading.DispatcherPriority.ApplicationIdle); - - var timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(100) }; - timer.Tick += (s, args) => { - timer.Stop(); - UpdateTypographyImage(); - }; - timer.Start(); - } - private void Hyperlink_RequestNavigate(object sender, RequestNavigateEventArgs e) { Process.Start(new ProcessStartInfo(e.Uri.AbsoluteUri) { UseShellExecute = true });