From dfb9907c9df4a03c018c8255e6cf5f351b4bcc8a Mon Sep 17 00:00:00 2001 From: Harald Daltveit Date: Sun, 7 Jun 2026 15:52:53 +0200 Subject: [PATCH 1/5] fix: add JSON fallback for SaveWindowPosition on .NET 9 - ApplicationSettingsBase.Save() fails on .NET 9 < 9.0.3 due to a runtime bug (dotnet/runtime#112410) where ClientConfigurationHost receives an empty path on UNC/network locations. - SaveWindowState() now saves a JSON backup to %LocalAppData% as a fallback when the settings save fails. - LoadWindowState() tries the JSON fallback when settings reload fails or returns an empty placement. - On successful save via ApplicationSettingsBase, the JSON backup is also updated to stay in sync. Fixes #4541 --- .../Behaviors/WindowsSettingBehavior.cs | 81 ++++++++++++++++++- 1 file changed, 80 insertions(+), 1 deletion(-) diff --git a/src/MahApps.Metro/Behaviors/WindowsSettingBehavior.cs b/src/MahApps.Metro/Behaviors/WindowsSettingBehavior.cs index 2e3b30c1e..193fe745a 100644 --- a/src/MahApps.Metro/Behaviors/WindowsSettingBehavior.cs +++ b/src/MahApps.Metro/Behaviors/WindowsSettingBehavior.cs @@ -4,7 +4,9 @@ using System; using System.Diagnostics; +using System.IO; using System.Runtime.InteropServices; +using System.Text.Json; using System.Windows; using System.Windows.Interop; using Windows.Win32; @@ -127,11 +129,21 @@ private void LoadWindowState() catch (Exception e) { Trace.TraceError($"{this}: The settings for {window} could not be reloaded! {e}"); - return; + // Don't return yet — try JSON fallback below } // check for existing placement and prevent empty bounds if (settings.Placement is null || settings.Placement.normalPosition.IsEmpty) + { + // Fallback: try to load from JSON backup if settings has no valid placement + if (TryLoadFromJsonFallback(window, out var fallbackPlacement)) + { + settings.Placement = fallbackPlacement; + } + } + + // If we still have no valid placement, nothing to restore + if (settings.Placement is null || settings.Placement.normalPosition.IsEmpty) { return; } @@ -210,11 +222,78 @@ private void SaveWindowState() try { settings.Save(); + + // On successful save, also save to JSON fallback for .NET version resilience + SaveToJsonFallback(window, settings.Placement); } catch (Exception e) { Trace.TraceError($"{this}: The settings could not be saved! {e}"); + // Fallback: save to JSON file when ApplicationSettingsBase.Save() fails + // (e.g. .NET 9 < 9.0.3 bug where ClientConfigurationHost fails on UNC paths) + try + { + SaveToJsonFallback(window, settings.Placement); + Trace.TraceInformation($"{this}: Window placement saved to JSON fallback instead."); + } + catch (Exception fallbackEx) + { + Trace.TraceError($"{this}: The JSON fallback save also failed! {fallbackEx}"); + } + } + } + private static readonly JsonSerializerOptions JsonOptions = new() + { + IncludeFields = true + }; + + private void SaveToJsonFallback(Window window, WindowPlacementSetting? placement) + { + if (placement is null) + { + return; } + + var filePath = GetFallbackFilePath(window); + var directory = Path.GetDirectoryName(filePath); + if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) + { + Directory.CreateDirectory(directory); + } + + var json = JsonSerializer.Serialize(placement, JsonOptions); + File.WriteAllText(filePath, json); + } + + private bool TryLoadFromJsonFallback(Window window, out WindowPlacementSetting? placement) + { + placement = null; + + try + { + var filePath = GetFallbackFilePath(window); + if (!File.Exists(filePath)) + { + return false; + } + + var json = File.ReadAllText(filePath); + placement = JsonSerializer.Deserialize(json, JsonOptions); + return placement != null; + } + catch (Exception ex) + { + Trace.TraceWarning($"{this}: Could not load JSON fallback for {window}: {ex.Message}"); + return false; + } + } + + private static string GetFallbackFilePath(Window window) + { + var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + var appName = System.Reflection.Assembly.GetEntryAssembly()?.GetName().Name ?? "MahApps.Metro"; + var windowTypeName = window.GetType().FullName?.Replace('.', '_') ?? "MetroWindow"; + return Path.Combine(localAppData, appName, "WindowPlacement", $"{windowTypeName}.json"); } } } \ No newline at end of file From 6c2465cd8886925e31e1ed4943dc7bca5b7b9c9e Mon Sep 17 00:00:00 2001 From: Harald Daltveit Date: Sun, 7 Jun 2026 16:17:42 +0200 Subject: [PATCH 2/5] fix: exclude System.Text.Json from net462 build System.Text.Json is not available on .NET Framework 4.6.2. Wrap JSON fallback code with #if !NET462 to maintain compatibility with legacy target framework. --- src/MahApps.Metro/Behaviors/WindowsSettingBehavior.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/MahApps.Metro/Behaviors/WindowsSettingBehavior.cs b/src/MahApps.Metro/Behaviors/WindowsSettingBehavior.cs index 193fe745a..df8f7d607 100644 --- a/src/MahApps.Metro/Behaviors/WindowsSettingBehavior.cs +++ b/src/MahApps.Metro/Behaviors/WindowsSettingBehavior.cs @@ -6,7 +6,9 @@ using System.Diagnostics; using System.IO; using System.Runtime.InteropServices; +#if !NET462 using System.Text.Json; +#endif using System.Windows; using System.Windows.Interop; using Windows.Win32; @@ -135,11 +137,13 @@ private void LoadWindowState() // check for existing placement and prevent empty bounds if (settings.Placement is null || settings.Placement.normalPosition.IsEmpty) { +#if !NET462 // Fallback: try to load from JSON backup if settings has no valid placement if (TryLoadFromJsonFallback(window, out var fallbackPlacement)) { settings.Placement = fallbackPlacement; } +#endif } // If we still have no valid placement, nothing to restore @@ -223,12 +227,15 @@ private void SaveWindowState() { settings.Save(); +#if !NET462 // On successful save, also save to JSON fallback for .NET version resilience SaveToJsonFallback(window, settings.Placement); +#endif } catch (Exception e) { Trace.TraceError($"{this}: The settings could not be saved! {e}"); +#if !NET462 // Fallback: save to JSON file when ApplicationSettingsBase.Save() fails // (e.g. .NET 9 < 9.0.3 bug where ClientConfigurationHost fails on UNC paths) try @@ -240,8 +247,11 @@ private void SaveWindowState() { Trace.TraceError($"{this}: The JSON fallback save also failed! {fallbackEx}"); } +#endif } } + +#if !NET462 private static readonly JsonSerializerOptions JsonOptions = new() { IncludeFields = true @@ -295,5 +305,6 @@ private static string GetFallbackFilePath(Window window) var windowTypeName = window.GetType().FullName?.Replace('.', '_') ?? "MetroWindow"; return Path.Combine(localAppData, appName, "WindowPlacement", $"{windowTypeName}.json"); } +#endif } } \ No newline at end of file From b0a34a8d29bb82c3a2d1a83431b5afb6d3f85222 Mon Sep 17 00:00:00 2001 From: Harald Daltveit Date: Sun, 7 Jun 2026 16:32:06 +0200 Subject: [PATCH 3/5] fix: remove duplicate placement check in LoadWindowState Merge the two identical null/empty checks into one to eliminate Codacy warning about redundant condition. --- .../Behaviors/WindowsSettingBehavior.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/MahApps.Metro/Behaviors/WindowsSettingBehavior.cs b/src/MahApps.Metro/Behaviors/WindowsSettingBehavior.cs index df8f7d607..7464d746b 100644 --- a/src/MahApps.Metro/Behaviors/WindowsSettingBehavior.cs +++ b/src/MahApps.Metro/Behaviors/WindowsSettingBehavior.cs @@ -143,13 +143,13 @@ private void LoadWindowState() { settings.Placement = fallbackPlacement; } -#endif - } - - // If we still have no valid placement, nothing to restore - if (settings.Placement is null || settings.Placement.normalPosition.IsEmpty) - { + else + { + return; + } +#else return; +#endif } try From 003d8bbe06d6797958dba452046b505a0b94f707 Mon Sep 17 00:00:00 2001 From: Harald Daltveit Date: Sun, 7 Jun 2026 16:42:04 +0200 Subject: [PATCH 4/5] fix: use local variable to satisfy nullable flow analysis Capture settings.Placement in a local variable so the compiler can track null-state through the #if blocks. Prevents CS8602 on placement.ToWINDOWPLACEMENT(). --- src/MahApps.Metro/Behaviors/WindowsSettingBehavior.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/MahApps.Metro/Behaviors/WindowsSettingBehavior.cs b/src/MahApps.Metro/Behaviors/WindowsSettingBehavior.cs index 7464d746b..d08e1272a 100644 --- a/src/MahApps.Metro/Behaviors/WindowsSettingBehavior.cs +++ b/src/MahApps.Metro/Behaviors/WindowsSettingBehavior.cs @@ -135,12 +135,14 @@ private void LoadWindowState() } // check for existing placement and prevent empty bounds - if (settings.Placement is null || settings.Placement.normalPosition.IsEmpty) + var placement = settings.Placement; + if (placement is null || placement.normalPosition.IsEmpty) { #if !NET462 // Fallback: try to load from JSON backup if settings has no valid placement if (TryLoadFromJsonFallback(window, out var fallbackPlacement)) { + placement = fallbackPlacement; settings.Placement = fallbackPlacement; } else @@ -154,7 +156,7 @@ private void LoadWindowState() try { - var wp = settings.Placement.ToWINDOWPLACEMENT(); + var wp = placement.ToWINDOWPLACEMENT(); WinApiHelper.SetWindowPlacement(window, wp); } catch (Exception ex) From 3680bfcce6b2f54db418e5776f32e101f366968b Mon Sep 17 00:00:00 2001 From: Harald Daltveit Date: Sun, 7 Jun 2026 17:00:54 +0200 Subject: [PATCH 5/5] fix: add null-forgiving operator for placement Compiler nullable flow analysis can't track through #if blocks and out-parameter narrowing. placement is guaranteed non-null after the if-check (we return in all null branches). --- src/MahApps.Metro/Behaviors/WindowsSettingBehavior.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MahApps.Metro/Behaviors/WindowsSettingBehavior.cs b/src/MahApps.Metro/Behaviors/WindowsSettingBehavior.cs index d08e1272a..60c46d4e3 100644 --- a/src/MahApps.Metro/Behaviors/WindowsSettingBehavior.cs +++ b/src/MahApps.Metro/Behaviors/WindowsSettingBehavior.cs @@ -156,7 +156,7 @@ private void LoadWindowState() try { - var wp = placement.ToWINDOWPLACEMENT(); + var wp = placement!.ToWINDOWPLACEMENT(); WinApiHelper.SetWindowPlacement(window, wp); } catch (Exception ex)