From ed81947235a734ec6447a9537ed5b5fa77da1b48 Mon Sep 17 00:00:00 2001 From: SeoJCalive <180237232+SeoJCalive@users.noreply.github.com> Date: Thu, 29 Oct 2026 13:52:30 +0900 Subject: [PATCH] Release the engine setup window however it is closed (#666) Closing the setup window with the title bar left its view model subscribed to the shared engine packages, so the window and view model were never collected. The window now releases those handlers on Closed; the Close command keeps doing it too. --- .../ViewModels/EngineSetupViewModel.cs | 7 +- .../Views/EngineSetupWindow.axaml.cs | 6 + .../EngineSetupReleaseTests.cs | 278 ++++++++++++++++++ 3 files changed, 290 insertions(+), 1 deletion(-) create mode 100644 tests/Filee.App.Tests/EngineSetupReleaseTests.cs diff --git a/src/Filee.App/ViewModels/EngineSetupViewModel.cs b/src/Filee.App/ViewModels/EngineSetupViewModel.cs index 1cd418ff1..9475b91d4 100644 --- a/src/Filee.App/ViewModels/EngineSetupViewModel.cs +++ b/src/Filee.App/ViewModels/EngineSetupViewModel.cs @@ -83,10 +83,15 @@ private void Start(IReadOnlyList packages) [RelayCommand] private void Close() + { + Release(); + CloseRequested?.Invoke(this, EventArgs.Empty); + } + + internal void Release() { foreach (var package in Packages) package.PropertyChanged -= OnPackageChanged; - CloseRequested?.Invoke(this, EventArgs.Empty); } private void OnPackageChanged(object? sender, PropertyChangedEventArgs e) diff --git a/src/Filee.App/Views/EngineSetupWindow.axaml.cs b/src/Filee.App/Views/EngineSetupWindow.axaml.cs index 96ea33d60..2d0351c85 100644 --- a/src/Filee.App/Views/EngineSetupWindow.axaml.cs +++ b/src/Filee.App/Views/EngineSetupWindow.axaml.cs @@ -22,6 +22,12 @@ public EngineSetupWindow() if (DataContext is EngineSetupViewModel vm) vm.CloseRequested += (_, _) => Close(); }; + Closed += (_, _) => + { + // The title bar closes the window without running the command. + if (DataContext is EngineSetupViewModel vm) + vm.Release(); + }; Opened += (_, _) => FitToScreen(); // Progress bars make the cards taller while engines install. SizeChanged += (_, e) => KeepInside(e.NewSize.Height); diff --git a/tests/Filee.App.Tests/EngineSetupReleaseTests.cs b/tests/Filee.App.Tests/EngineSetupReleaseTests.cs new file mode 100644 index 000000000..840414987 --- /dev/null +++ b/tests/Filee.App.Tests/EngineSetupReleaseTests.cs @@ -0,0 +1,278 @@ +using System.ComponentModel; +using System.Reflection; +using System.Runtime.CompilerServices; +using Avalonia.Controls; +using Avalonia.Headless; +using Avalonia.Headless.XUnit; +using Avalonia.Input; +using Avalonia.Input.Raw; +using Avalonia.Threading; +using Avalonia.VisualTree; +using Filee.App.Services; +using Filee.App.ViewModels; +using Filee.App.Views; +using Filee.Core.Localization; + +namespace Filee.App.Tests; + +[Collection("MainWindow")] +public class EngineSetupReleaseTests +{ + private static readonly FieldInfo SetupWindow = typeof(WindowService) + .GetField("_engineSetup", BindingFlags.Instance | BindingFlags.NonPublic)!; + private static readonly FieldInfo PackageChanged = typeof(EnginePackageState).BaseType! + .GetField("PropertyChanged", BindingFlags.Instance | BindingFlags.NonPublic)!; + + [AvaloniaTheory] + [InlineData(false)] + [InlineData(true)] + public void Closing_setup_releases_the_window_and_view_model(bool command) + { + TestServices.EnsureInitialized("en"); + var service = AppHost.Get(); + var states = SavePackages(); + var baseline = HandlerCounts(); + var closed = new List<(WeakReference Window, WeakReference Model)>(); + try + { + for (var i = 0; i < 3; i++) + { + closed.Add(OpenAndClose(service, command ? "command" : "direct", baseline)); + AssertCollected(closed); + } + } + finally + { + (SetupWindow.GetValue(service) as EngineSetupWindow)?.Close(); + RestorePackages(states); + } + } + + [AvaloniaTheory] + [InlineData("direct")] + [InlineData("command")] + [InlineData("escape")] + public void Directly_created_setup_releases_on_close(string path) + { + TestServices.EnsureInitialized("en"); + var states = SavePackages(); + var baseline = HandlerCounts(); + try + { + var closed = OpenAndClose(null, path, baseline); + AssertCollected([closed]); + } + finally + { + RestorePackages(states); + } + } + + [AvaloniaFact] + public void Releasing_setup_twice_preserves_other_subscribers() + { + TestServices.EnsureInitialized("en"); + var states = SavePackages(); + var baseline = HandlerCounts(); + var other = NewModel(); + var target = NewModel(); + try + { + AssertHandlers(baseline, 2, target); + target.Release(); + target.Release(); + AssertHandlers(baseline, 1, other); + Assert.All(target.Packages, p => Assert.DoesNotContain(SetupHandlers(p), d => ReferenceEquals(d.Target, target))); + } + finally + { + target.Release(); + other.Release(); + RestorePackages(states); + } + AssertHandlers(baseline, 0); + } + + [AvaloniaFact] + public void Open_setup_keeps_selection_and_close_text_in_sync() + { + TestServices.EnsureInitialized("en"); + var states = SavePackages(); + var baseline = HandlerCounts(); + var vm = NewModel(); + var window = new EngineSetupWindow { DataContext = vm }; + try + { + window.Show(); + Pump(); + var package = vm.Packages.First(p => p.CanInstall); + var check = window.GetVisualDescendants().OfType() + .Single(c => ReferenceEquals(c.DataContext, package)); + var install = window.FindControl