Skip to content

fix: address remaining review findings from PR #55's floating feature - #58

Merged
NeverMorewd merged 1 commit into
mainfrom
fix/floating-followups
Oct 10, 2026
Merged

NeverMorewd merged 1 commit into
mainfrom
fix/floating-followups

Conversation

@NeverMorewd

Copy link
Copy Markdown
Owner

Summary

Follow-up to #55/#57. Fixes 5 of the 6 previously-deferred review findings on the floating feature (pre-existing, not introduced by #57's contended-lease fix); the 6th turned out to be a false positive once checked against an existing test.

  • RegionNavigationService.OnAfterNavigationAsync: call OnNavigatedFromAsync before the pipeline's cancellation check throws, if cancellation raced in right after OnNavigatedToAsync succeeded — otherwise the view's lifecycle state (told it's activated, never told it left) goes out of sync with the region reverting to whatever was active before. Covered by a new regression test (Navigation_CancelledRightAfterActivation_StillCallsOnNavigatedFrom) that reproduces the exact race via a test-double activation hook.
  • WeakUnloadObserver.Subscribe: takes Func<INavigationAware, Task> instead of Action<INavigationAware> and awaits it, so its one caller's async lambda is a real awaited continuation instead of async-void with unobservable exceptions.
  • ViewManager.Remove(key, dispose): now returns bool (true if it actually removed something) instead of silently no-oping when the entry is pinned by a floating view, so callers can tell the two cases apart. (IViewManager.Remove signature updated to match — source-compatible for every existing caller, which all discard the return value.)
  • ViewManager.TrimCache: resumes scanning from where the previous eviction left off instead of restarting from the LRU tail every outer-loop iteration, avoiding O(evictions × pinned-views) work when floating views sit at the tail.
  • FloatingWindowCloseGuard (new, in AsyncNavigation.Floating): extracted the "cancel the window's close, then raise CloseRequested" state machine that was duplicated near-verbatim between AvaloniaFloatingWindowHost and WpfFloatingWindowHost, so this exact subtle dance only has to be gotten right once.

Not changed: ViewManager.AddView's UpdateDuplicateKey path overwriting a still-floating view's cache slot. The review flagged this as orphaning the floating instance from the cache map, but CloseOldInstance_DoesNotRemoveNewInstanceCacheOrHistory already tests and requires exactly this behavior (the fresh instance must become the authoritative cache entry so it's reusable once the old floating one closes) — applying the suggested guard broke that test, confirming the original behavior is intentional, not a bug.

Test plan

  • dotnet test tests/AsyncNavigation.Tests — 158 passed (Debug and Release)
  • dotnet test tests/AsyncNavigation.E2E.Tests — 9 passed (Debug and Release)
  • New regression test for the cancel-after-activate race, verified it fails without the fix and passes with it
  • Full solution build — 0 errors
  • WPF/Avalonia floating host refactor is a pure mechanical extraction of identical logic (no behavior change); Avalonia's side of it is exercised by the existing FloatingWindowLifecycleTests.Dock_and_close_raise_separate_requests headless test

Co-Authored-By: Claude Sonnet 5 noreply@anthropic.com

Fixes 5 of the 6 previously-deferred, pre-existing (not introduced by the
earlier floating-feature fixes) review findings; the 6th (ViewManager.AddView's
UpdateDuplicateKey path overwriting a still-floating view's cache slot) turned
out to be intended, tested behavior once verified against
CloseOldInstance_DoesNotRemoveNewInstanceCacheOrHistory, so it was left as-is.

- RegionNavigationService.OnAfterNavigationAsync: call OnNavigatedFromAsync
  before the pipeline's cancellation check throws, if cancellation raced in
  right after OnNavigatedToAsync succeeded - otherwise the view's lifecycle
  state (activated, never told it left) goes out of sync with the region
  reverting. Covered by a new regression test that reproduces the race via a
  test double's activation hook.
- WeakUnloadObserver.Subscribe: take Func<INavigationAware, Task> instead of
  Action<INavigationAware> and await it, so its one caller's async lambda is a
  real awaited continuation instead of async-void with unobservable exceptions.
- ViewManager: Remove(key, dispose) now returns bool (true if it actually
  removed something) instead of silently no-op'ing when the entry is pinned by
  a floating view, so callers can tell the two cases apart.
- ViewManager.TrimCache: resume scanning from where the previous eviction left
  off instead of restarting from the LRU tail every iteration, avoiding
  O(evictions * pinned-views) work when floating views sit at the tail.
- Extracted the "cancel the window's close, then raise CloseRequested" state
  machine - duplicated near-verbatim between AvaloniaFloatingWindowHost and
  WpfFloatingWindowHost - into a shared FloatingWindowCloseGuard in
  AsyncNavigation.Floating, so this exact subtle dance only needs fixing once.

Verified: dotnet test tests/AsyncNavigation.Tests (158 passed) and
tests/AsyncNavigation.E2E.Tests (9 passed), Debug and Release, plus a full
solution build - all green.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@NeverMorewd
NeverMorewd merged commit a046401 into main Oct 10, 2026
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant