diff --git a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs index 29572143083..37e365ea69e 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Runtime.CompilerServices; using System.Threading; using Avalonia.Collections; @@ -184,11 +183,12 @@ private bool DoShutdown( // When an OS shutdown request is received, try to close all non-owned windows. Windows can cancel // shutdown by setting e.Cancel = true in the Closing event. Owned windows will be shutdown by their // owners. - foreach (var w in Windows.ToArray()) + foreach (var w in new List(_windows)) { if (w.Owner is null) { - w.CloseCore(WindowCloseReason.ApplicationShutdown, isProgrammatic); + var ignoreCancel = force || (ShutdownMode == ShutdownMode.OnMainWindowClose && w != MainWindow); + w.CloseCore(WindowCloseReason.ApplicationShutdown, isProgrammatic, ignoreCancel); } } diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 0e201665127..2b21c1f37b2 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -461,7 +461,7 @@ public PixelPoint Position /// public void Close() { - CloseCore(WindowCloseReason.WindowClosing, true); + CloseCore(WindowCloseReason.WindowClosing, true, false); } /// @@ -477,10 +477,10 @@ public void Close() public void Close(object? dialogResult) { _dialogResult = dialogResult; - CloseCore(WindowCloseReason.WindowClosing, true); + CloseCore(WindowCloseReason.WindowClosing, true, false); } - internal void CloseCore(WindowCloseReason reason, bool isProgrammatic) + internal void CloseCore(WindowCloseReason reason, bool isProgrammatic, bool ignoreCancel) { bool close = true; @@ -493,7 +493,7 @@ internal void CloseCore(WindowCloseReason reason, bool isProgrammatic) } finally { - if (close) + if (close || ignoreCancel) { CloseInternal(); } @@ -1113,10 +1113,12 @@ protected sealed override Size ArrangeSetBounds(Size size) private protected sealed override void HandleClosed() { - RaiseEvent(new RoutedEventArgs(WindowClosedEvent)); + _shown = false; base.HandleClosed(); + RaiseEvent(new RoutedEventArgs(WindowClosedEvent)); + Owner = null; } diff --git a/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs b/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs index 936cfbd9643..f015ca71280 100644 --- a/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs +++ b/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs @@ -126,6 +126,86 @@ public void Should_Exit_After_MainWindow_Closed() } } + [Fact] + public void OnMainWindowClose_Overrides_Secondary_Window_Cancellation() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + using(var lifetime = new ClassicDesktopStyleApplicationLifetime()) + { + lifetime.ShutdownMode = ShutdownMode.OnMainWindowClose; + lifetime.SetupCore(Array.Empty()); + + var hasExit = false; + var secondaryWindowClosingExecuted = false; + var secondaryWindowClosedExecuted = false; + + lifetime.Exit += (_, _) => hasExit = true; + + var mainWindow = new Window(); + mainWindow.Show(); + + lifetime.MainWindow = mainWindow; + + var window = new Window(); + window.Closing += (_, args) => + { + secondaryWindowClosingExecuted = true; + args.Cancel = true; + }; + window.Closed += (_, _) => + { + secondaryWindowClosedExecuted = true; + }; + window.Show(); + + mainWindow.Close(); + + Assert.True(secondaryWindowClosingExecuted); + Assert.True(secondaryWindowClosedExecuted); + Assert.True(hasExit); + } + } + + [Fact] + public void OnMainWindowClose_Overrides_Secondary_Window_Cancellation_From_TryShutdown() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + using(var lifetime = new ClassicDesktopStyleApplicationLifetime()) + { + lifetime.ShutdownMode = ShutdownMode.OnMainWindowClose; + lifetime.SetupCore(Array.Empty()); + + var hasExit = false; + var secondaryWindowClosingExecuted = false; + var secondaryWindowClosedExecuted = false; + + lifetime.Exit += (_, _) => hasExit = true; + + var mainWindow = new Window(); + mainWindow.Show(); + + lifetime.MainWindow = mainWindow; + + var window = new Window(); + window.Closing += (_, args) => + { + secondaryWindowClosingExecuted = true; + args.Cancel = true; + }; + window.Closed += (_, _) => + { + secondaryWindowClosedExecuted = true; + }; + window.Show(); + + lifetime.TryShutdown(); + + Assert.True(secondaryWindowClosingExecuted); + Assert.True(secondaryWindowClosedExecuted); + Assert.True(hasExit); + } + } + [Fact] public void Should_Exit_After_Last_Window_Closed() { @@ -401,6 +481,8 @@ public void Shutdown_NotCancellable_By_Preventing_Window_Close() lifetime.SetupCore(Array.Empty()); var hasExit = false; + var closingRaised = 0; + var closedRaised = 0; lifetime.Exit += (_, _) => hasExit = true; @@ -411,18 +493,21 @@ public void Shutdown_NotCancellable_By_Preventing_Window_Close() var windowB = new Window(); windowB.Show(); - - var raised = 0; windowA.Closing += (_, e) => { e.Cancel = true; - ++raised; + ++closingRaised; + }; + windowA.Closed += (_, e) => + { + ++closedRaised; }; lifetime.Shutdown(); - Assert.Equal(1, raised); + Assert.Equal(1, closingRaised); + Assert.Equal(1, closedRaised); Assert.True(hasExit); } } diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs index 421e0b5977b..30d9cec449f 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs @@ -112,6 +112,8 @@ public void IsVisible_Should_Be_False_After_Impl_Signals_Close() var window = new Window(); window.Show(); + Assert.True(window.IsVisible); + windowImpl.Object.Closed(); Assert.False(window.IsVisible);