diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index 329b757ec4..acd9534d14 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -45,14 +45,6 @@ namespace Avalonia private Styles _styles; private IResourceDictionary _resources; - /// - /// Initializes a new instance of the class. - /// - public Application() - { - Windows = new WindowCollection(this); - } - /// public event EventHandler ResourcesChanged; @@ -159,14 +151,6 @@ namespace Avalonia /// IResourceNode IResourceNode.ResourceParent => null; - - /// - /// Gets the open windows of the application. - /// - /// - /// The windows. - /// - public WindowCollection Windows { get; } /// /// Application lifetime, use it for things like setting the main window and exiting the app from code diff --git a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs index d6d5c56537..abca7a64ee 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs @@ -1,21 +1,46 @@ using System; +using System.Collections.Generic; +using System.Linq; using System.Threading; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Interactivity; namespace Avalonia.Controls.ApplicationLifetimes { - public class ClassicDesktopStyleApplicationLifetime : IClassicDesktopStyleApplicationLifetime + public class ClassicDesktopStyleApplicationLifetime : IClassicDesktopStyleApplicationLifetime, IDisposable { private readonly Application _app; private int _exitCode; private CancellationTokenSource _cts; private bool _isShuttingDown; + private HashSet _windows = new HashSet(); + + private static ClassicDesktopStyleApplicationLifetime _activeLifetime; + static ClassicDesktopStyleApplicationLifetime() + { + Window.WindowOpenedEvent.AddClassHandler(typeof(Window), OnWindowOpened); + Window.WindowClosedEvent.AddClassHandler(typeof(Window), WindowClosedEvent); + } + + private static void WindowClosedEvent(object sender, RoutedEventArgs e) + { + _activeLifetime?._windows.Remove((Window)sender); + _activeLifetime?.HandleWindowClosed((Window)sender); + } + + private static void OnWindowOpened(object sender, RoutedEventArgs e) + { + _activeLifetime?._windows.Add((Window)sender); + } public ClassicDesktopStyleApplicationLifetime(Application app) { + if (_activeLifetime != null) + throw new InvalidOperationException( + "Can not have multiple active ClassicDesktopStyleApplicationLifetime instances and the previously created one was not disposed"); _app = app; - app.Windows.OnWindowClosed += HandleWindowClosed; + _activeLifetime = this; } /// @@ -29,6 +54,8 @@ namespace Avalonia.Controls.ApplicationLifetimes /// public Window MainWindow { get; set; } + public IReadOnlyList Windows => _windows.ToList(); + private void HandleWindowClosed(Window window) { if (window == null) @@ -37,7 +64,7 @@ namespace Avalonia.Controls.ApplicationLifetimes if (_isShuttingDown) return; - if (ShutdownMode == ShutdownMode.OnLastWindowClose && _app.Windows.Count == 0) + if (ShutdownMode == ShutdownMode.OnLastWindowClose && _windows.Count == 0) Shutdown(); else if (ShutdownMode == ShutdownMode.OnMainWindowClose && window == MainWindow) Shutdown(); @@ -56,7 +83,8 @@ namespace Avalonia.Controls.ApplicationLifetimes try { - _app.Windows.CloseAll(); + foreach (var w in Windows) + w.Close(); var e = new ControlledApplicationLifetimeExitEventArgs(exitCode); Exit?.Invoke(this, e); _exitCode = e.ApplicationExitCode; @@ -79,6 +107,12 @@ namespace Avalonia.Controls.ApplicationLifetimes Environment.ExitCode = _exitCode; return _exitCode; } + + public void Dispose() + { + if (_activeLifetime == this) + _activeLifetime = null; + } } } diff --git a/src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs index 6d809c6714..a1006d907b 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; namespace Avalonia.Controls.ApplicationLifetimes { @@ -24,5 +25,7 @@ namespace Avalonia.Controls.ApplicationLifetimes /// The main window. /// Window MainWindow { get; set; } + + IReadOnlyList Windows { get; } } } diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 7ae0380ba0..5c117f508b 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -14,6 +14,7 @@ using System.Collections.Generic; using System.Linq; using JetBrains.Annotations; using System.ComponentModel; +using Avalonia.Interactivity; namespace Avalonia.Controls { @@ -97,6 +98,20 @@ namespace Avalonia.Controls public static readonly StyledProperty CanResizeProperty = AvaloniaProperty.Register(nameof(CanResize), true); + /// + /// Routed event that can be used for global tracking of window destruction + /// + public static readonly RoutedEvent WindowClosedEvent = + RoutedEvent.Register("WindowClosed", RoutingStrategies.Direct); + + /// + /// Routed event that can be used for global tracking of opening windows + /// + public static readonly RoutedEvent WindowOpenedEvent = + RoutedEvent.Register("WindowOpened", RoutingStrategies.Direct); + + + private readonly NameScope _nameScope = new NameScope(); private object _dialogResult; private readonly Size _maxPlatformClientSize; @@ -249,26 +264,6 @@ namespace Avalonia.Controls /// public event EventHandler Closing; - private static void AddWindow(Window window) - { - if (Application.Current == null) - { - return; - } - - Application.Current.Windows.Add(window); - } - - private static void RemoveWindow(Window window) - { - if (Application.Current == null) - { - return; - } - - Application.Current.Windows.Remove(window); - } - /// /// Closes the window. /// @@ -376,7 +371,7 @@ namespace Avalonia.Controls return; } - AddWindow(this); + this.RaiseEvent(new RoutedEventArgs(WindowOpenedEvent)); EnsureInitialized(); IsVisible = true; @@ -438,7 +433,7 @@ namespace Avalonia.Controls throw new InvalidOperationException("The window is already being shown."); } - AddWindow(this); + RaiseEvent(new RoutedEventArgs(WindowOpenedEvent)); EnsureInitialized(); IsVisible = true; @@ -551,7 +546,7 @@ namespace Avalonia.Controls protected override void HandleClosed() { - RemoveWindow(this); + RaiseEvent(new RoutedEventArgs(WindowClosedEvent)); base.HandleClosed(); } diff --git a/src/Avalonia.Controls/WindowCollection.cs b/src/Avalonia.Controls/WindowCollection.cs deleted file mode 100644 index aa076a1808..0000000000 --- a/src/Avalonia.Controls/WindowCollection.cs +++ /dev/null @@ -1,111 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System; -using System.Collections; -using System.Collections.Generic; - -using Avalonia.Controls; - -namespace Avalonia -{ - public class WindowCollection : IReadOnlyList - { - private readonly Application _application; - private readonly List _windows = new List(); - public event Action OnWindowClosed; - - public WindowCollection(Application application) - { - _application = application; - } - - /// - /// - /// Gets the number of elements in the collection. - /// - public int Count => _windows.Count; - - /// - /// - /// Gets the at the specified index. - /// - /// - /// The . - /// - /// The index. - /// - public Window this[int index] => _windows[index]; - - /// - /// - /// Returns an enumerator that iterates through the collection. - /// - /// - /// An enumerator that can be used to iterate through the collection. - /// - public IEnumerator GetEnumerator() - { - return _windows.GetEnumerator(); - } - - /// - /// - /// Returns an enumerator that iterates through a collection. - /// - /// - /// An object that can be used to iterate through the collection. - /// - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - - /// - /// Adds the specified window. - /// - /// The window. - internal void Add(Window window) - { - if (window == null) - { - return; - } - - _windows.Add(window); - } - - /// - /// Removes the specified window. - /// - /// The window. - internal void Remove(Window window) - { - if (window == null) - { - return; - } - - _windows.Remove(window); - - OnRemoveWindow(window); - } - - /// - /// Closes all windows and removes them from the underlying collection. - /// - public void CloseAll() - { - while (_windows.Count > 0) - { - _windows[0].Close(true); - } - } - - private void OnRemoveWindow(Window window) - { - if (window != null) - OnWindowClosed?.Invoke(window); - } - } -} diff --git a/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs b/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs index ee27d6493b..74523d4193 100644 --- a/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs +++ b/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs @@ -1,8 +1,10 @@ using System; using System.Collections.Generic; using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Platform; using Avalonia.Threading; using Avalonia.UnitTests; +using Moq; using Xunit; namespace Avalonia.Controls.UnitTests @@ -14,9 +16,8 @@ namespace Avalonia.Controls.UnitTests public void Should_Set_ExitCode_After_Shutdown() { using (UnitTestApplication.Start(TestServices.MockThreadingInterface)) + using(var lifetime = new ClassicDesktopStyleApplicationLifetime(Application.Current)) { - var lifetime = new ClassicDesktopStyleApplicationLifetime(Application.Current); - Dispatcher.UIThread.InvokeAsync(() => lifetime.Shutdown(1337)); lifetime.Shutdown(1337); var exitCode = lifetime.Start(Array.Empty()); @@ -30,6 +31,7 @@ namespace Avalonia.Controls.UnitTests public void Should_Close_All_Remaining_Open_Windows_After_Explicit_Exit_Call() { using (UnitTestApplication.Start(TestServices.StyledWindow)) + using(var lifetime = new ClassicDesktopStyleApplicationLifetime(Application.Current)) { var windows = new List { new Window(), new Window(), new Window(), new Window() }; @@ -37,9 +39,10 @@ namespace Avalonia.Controls.UnitTests { window.Show(); } - new ClassicDesktopStyleApplicationLifetime(Application.Current).Shutdown(); + Assert.Equal(4, lifetime.Windows.Count); + lifetime.Shutdown(); - Assert.Empty(Application.Current.Windows); + Assert.Empty(lifetime.Windows); } } @@ -47,8 +50,8 @@ namespace Avalonia.Controls.UnitTests public void Should_Only_Exit_On_Explicit_Exit() { using (UnitTestApplication.Start(TestServices.StyledWindow)) + using(var lifetime = new ClassicDesktopStyleApplicationLifetime(Application.Current)) { - var lifetime = new ClassicDesktopStyleApplicationLifetime(Application.Current); lifetime.ShutdownMode = ShutdownMode.OnExplicitShutdown; var hasExit = false; @@ -81,8 +84,8 @@ namespace Avalonia.Controls.UnitTests public void Should_Exit_After_MainWindow_Closed() { using (UnitTestApplication.Start(TestServices.StyledWindow)) + using(var lifetime = new ClassicDesktopStyleApplicationLifetime(Application.Current)) { - var lifetime = new ClassicDesktopStyleApplicationLifetime(Application.Current); lifetime.ShutdownMode = ShutdownMode.OnMainWindowClose; var hasExit = false; @@ -109,8 +112,8 @@ namespace Avalonia.Controls.UnitTests public void Should_Exit_After_Last_Window_Closed() { using (UnitTestApplication.Start(TestServices.StyledWindow)) + using(var lifetime = new ClassicDesktopStyleApplicationLifetime(Application.Current)) { - var lifetime = new ClassicDesktopStyleApplicationLifetime(Application.Current); lifetime.ShutdownMode = ShutdownMode.OnLastWindowClose; var hasExit = false; @@ -134,6 +137,77 @@ namespace Avalonia.Controls.UnitTests Assert.True(hasExit); } } + + [Fact] + public void Show_Should_Add_Window_To_OpenWindows() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + using(var lifetime = new ClassicDesktopStyleApplicationLifetime(Application.Current)) + { + var window = new Window(); + + window.Show(); + + Assert.Equal(new[] { window }, lifetime.Windows); + } + } + + [Fact] + public void Window_Should_Be_Added_To_OpenWindows_Only_Once() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + using(var lifetime = new ClassicDesktopStyleApplicationLifetime(Application.Current)) + { + var window = new Window(); + + window.Show(); + window.Show(); + window.IsVisible = true; + + Assert.Equal(new[] { window }, lifetime.Windows); + + window.Close(); + } + } + + [Fact] + public void Close_Should_Remove_Window_From_OpenWindows() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + using(var lifetime = new ClassicDesktopStyleApplicationLifetime(Application.Current)) + { + var window = new Window(); + + window.Show(); + Assert.Equal(1, lifetime.Windows.Count); + window.Close(); + + Assert.Empty(lifetime.Windows); + } + } + + [Fact] + public void Impl_Closing_Should_Remove_Window_From_OpenWindows() + { + var windowImpl = new Mock(); + windowImpl.SetupProperty(x => x.Closed); + windowImpl.Setup(x => x.Scaling).Returns(1); + + var services = TestServices.StyledWindow.With( + windowingPlatform: new MockWindowingPlatform(() => windowImpl.Object)); + + using (UnitTestApplication.Start(services)) + using(var lifetime = new ClassicDesktopStyleApplicationLifetime(Application.Current)) + { + var window = new Window(); + + window.Show(); + Assert.Equal(1, lifetime.Windows.Count); + windowImpl.Object.Closed(); + + Assert.Empty(lifetime.Windows); + } + } } } diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs index 35f60e92cd..f4d9a91d0c 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs @@ -121,75 +121,6 @@ namespace Avalonia.Controls.UnitTests } } - [Fact] - public void Show_Should_Add_Window_To_OpenWindows() - { - using (UnitTestApplication.Start(TestServices.StyledWindow)) - { - ClearOpenWindows(); - var window = new Window(); - - window.Show(); - - Assert.Equal(new[] { window }, Application.Current.Windows); - } - } - - [Fact] - public void Window_Should_Be_Added_To_OpenWindows_Only_Once() - { - using (UnitTestApplication.Start(TestServices.StyledWindow)) - { - ClearOpenWindows(); - var window = new Window(); - - window.Show(); - window.Show(); - window.IsVisible = true; - - Assert.Equal(new[] { window }, Application.Current.Windows); - - window.Close(); - } - } - - [Fact] - public void Close_Should_Remove_Window_From_OpenWindows() - { - using (UnitTestApplication.Start(TestServices.StyledWindow)) - { - ClearOpenWindows(); - var window = new Window(); - - window.Show(); - window.Close(); - - Assert.Empty(Application.Current.Windows); - } - } - - [Fact] - public void Impl_Closing_Should_Remove_Window_From_OpenWindows() - { - var windowImpl = new Mock(); - windowImpl.SetupProperty(x => x.Closed); - windowImpl.Setup(x => x.Scaling).Returns(1); - - var services = TestServices.StyledWindow.With( - windowingPlatform: new MockWindowingPlatform(() => windowImpl.Object)); - - using (UnitTestApplication.Start(services)) - { - ClearOpenWindows(); - var window = new Window(); - - window.Show(); - windowImpl.Object.Closed(); - - Assert.Empty(Application.Current.Windows); - } - } - [Fact] public void Closing_Should_Only_Be_Invoked_Once() { @@ -420,12 +351,5 @@ namespace Avalonia.Controls.UnitTests x.Scaling == 1 && x.CreateRenderer(It.IsAny()) == renderer.Object); } - - private void ClearOpenWindows() - { - // HACK: We really need a decent way to have "statics" that can be scoped to - // AvaloniaLocator scopes. - Application.Current.Windows.CloseAll(); - } } }