From 0181e5600d12c889994b03df6ab92342a011ba31 Mon Sep 17 00:00:00 2001 From: Easley Date: Wed, 30 Sep 2026 13:24:08 +0200 Subject: [PATCH 1/7] fix: preserve NavigationId across floating restore Restoring a floated view minted a brand-new NavigationContext with a fresh NavigationId, so a view floated again after being docked no longer matched the id its own command had captured, causing ContentRegion/ItemsRegion/TabRegion.Capture to throw. That exception propagated as an unhandled dispatcher exception and crashed the app. RegionBase.RestorePlacementAsync now carries the original NavigationId into the restored context. Also tightens the FloatingNavigationTests fixture to validate navigationId the way the real regions do (it previously ignored it, masking the bug) and adds a regression test for float -> restore -> float again. --- src/AsyncNavigation/NavigationContext.cs | 2 +- src/AsyncNavigation/RegionBase.cs | 3 ++- .../FloatingNavigationTests.cs | 27 +++++++++++++++++-- 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/src/AsyncNavigation/NavigationContext.cs b/src/AsyncNavigation/NavigationContext.cs index 6ed7e83..7f40509 100644 --- a/src/AsyncNavigation/NavigationContext.cs +++ b/src/AsyncNavigation/NavigationContext.cs @@ -117,7 +117,7 @@ private set /// /// Gets a unique identifier for this navigation context. /// - public Guid NavigationId { get; } = Guid.NewGuid(); + public Guid NavigationId { get; init; } = Guid.NewGuid(); /// /// Gets the duration of the navigation operation. diff --git a/src/AsyncNavigation/RegionBase.cs b/src/AsyncNavigation/RegionBase.cs index 3567b44..aab771f 100644 --- a/src/AsyncNavigation/RegionBase.cs +++ b/src/AsyncNavigation/RegionBase.cs @@ -175,7 +175,8 @@ public async Task RestorePlacementAsync(RegionPlacementItem item, Func tra { RegionName = Name, ViewName = item.Context.ViewName, - Parameters = item.Context.Parameters + Parameters = item.Context.Parameters, + NavigationId = item.Context.NavigationId }; context.Target.Value = item.Context.Target.Value!; context.IndicatorHost.Value = item.Context.IndicatorHost.Value!; diff --git a/tests/AsyncNavigation.Tests/FloatingNavigationTests.cs b/tests/AsyncNavigation.Tests/FloatingNavigationTests.cs index 11f9d2d..1a131f1 100644 --- a/tests/AsyncNavigation.Tests/FloatingNavigationTests.cs +++ b/tests/AsyncNavigation.Tests/FloatingNavigationTests.cs @@ -58,6 +58,24 @@ public async Task DockIntoOccupiedRegion_ReusesOriginalAndUpdatesCurrentAndHisto Assert.Equal(1, a.Model.FromCount); } + [Fact] + public async Task RestoredView_KeepsOriginalNavigationIdAndCanBeFloatedAgain() + { + using var f = new Fixture(); + var a = await f.Navigate("A"); + var navigationId = f.Region.Selected!.NavigationId; + + var session = await f.Placement.FloatAsync("main", navigationId); + await session.RestoreAsync(); + + Assert.Equal(navigationId, f.Region.Selected!.NavigationId); + + var secondSession = await f.Placement.FloatAsync("main", navigationId); + + Assert.Equal(ViewPlacementState.Floating, secondSession.State); + Assert.Same(a, f.Windows[1].Content); + } + [Theory] [InlineData(false)] [InlineData(true)] @@ -263,8 +281,13 @@ public override Task ProcessDeactivateAsync(NavigationContext? context) if (context is null || ReferenceEquals(Selected, context)) Selected = null; return Task.CompletedTask; } - public RegionPlacementItem Capture(Guid? id = null) => - new(Selected ?? throw new InvalidOperationException("empty"), 0, true); + public RegionPlacementItem Capture(Guid? id = null) + { + var context = Selected ?? throw new InvalidOperationException("empty"); + if (id.HasValue && context.NavigationId != id.Value) + throw new InvalidOperationException($"Region '{Name}' does not contain the requested navigation item."); + return new(context, 0, true); + } public void Detach(RegionPlacementItem item) { Assert.Same(Selected, item.Context); From e6a15a22f21c2b2d81cc8eb9098d754ffdba351d Mon Sep 17 00:00:00 2001 From: Easley Date: Wed, 30 Sep 2026 13:24:20 +0200 Subject: [PATCH 2/7] feat(samples): move Float trigger onto content views, fix floating Unload Float used to be a button on each container view (MainWindow, TabRegionView, ChildContentRegionView, ItemsRegionView, ListBoxRegionView), each wired to its own Click handler and an IViewPlacementService constructor parameter. Moved it onto LightView and HeavyView themselves, mirroring how UnloadView/Dock already work - the view controls its own floating instead of a parent container doing it on its behalf. Container views and their code-behind are back to parameterless constructors with no placement wiring. Also fixes UnloadView doing nothing while a view is floating: it now closes the floating session (dispose + forget) when one exists for the view's NavigationId, falling back to the normal region-deactivate path otherwise. Applied identically to both Sample.Avalonia and Sample.Wpf. --- samples/Sample.Avalonia/App.axaml | 10 +++++ samples/Sample.Avalonia/App.axaml.cs | 4 +- .../Views/ChildContentRegionView.axaml | 4 -- .../Views/ChildContentRegionView.axaml.cs | 33 +--------------- samples/Sample.Avalonia/Views/HeavyView.axaml | 1 + .../Views/ItemsRegionView.axaml | 4 -- .../Views/ItemsRegionView.axaml.cs | 34 +---------------- .../Views/ListBoxRegionView.axaml | 4 -- .../Views/ListBoxRegionView.axaml.cs | 33 +--------------- samples/Sample.Avalonia/Views/MainView.axaml | 12 ++---- .../Sample.Avalonia/Views/MainView.axaml.cs | 37 +----------------- .../Sample.Avalonia/Views/MainWindow.axaml.cs | 15 ++------ .../Sample.Avalonia/Views/TabRegionView.axaml | 21 ---------- .../Views/TabRegionView.axaml.cs | 35 +---------------- .../Views/TabRegionViewModel.axaml | 1 + samples/Sample.Common/HeavyViewModel.cs | 16 +++++++- samples/Sample.Common/LightViewModel.cs | 13 ++++++- samples/Sample.Common/Sample.Common.csproj | 1 + samples/Sample.Common/ViewModelBase.cs | 12 ++++++ samples/Sample.Wpf/App.xaml | 10 +++++ samples/Sample.Wpf/App.xaml.cs | 2 +- samples/Sample.Wpf/MainWindow.xaml | 4 -- samples/Sample.Wpf/MainWindow.xaml.cs | 36 +----------------- .../Views/ChildContentRegionView.xaml | 4 -- .../Views/ChildContentRegionView.xaml.cs | 31 +-------------- samples/Sample.Wpf/Views/HeavyView.xaml | 1 + samples/Sample.Wpf/Views/ItemsRegionView.xaml | 4 -- .../Sample.Wpf/Views/ItemsRegionView.xaml.cs | 38 ++----------------- samples/Sample.Wpf/Views/LightView.xaml | 1 + .../Sample.Wpf/Views/ListBoxRegionView.xaml | 4 -- .../Views/ListBoxRegionView.xaml.cs | 31 +-------------- samples/Sample.Wpf/Views/TabRegionView.xaml | 21 ---------- .../Sample.Wpf/Views/TabRegionView.xaml.cs | 36 +----------------- 33 files changed, 89 insertions(+), 424 deletions(-) diff --git a/samples/Sample.Avalonia/App.axaml b/samples/Sample.Avalonia/App.axaml index d0520c1..9d59de9 100644 --- a/samples/Sample.Avalonia/App.axaml +++ b/samples/Sample.Avalonia/App.axaml @@ -43,6 +43,16 @@ + + + +