From a83d0edae751ac52033f613a4e72018b3ec7a741 Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Fri, 25 May 2018 13:04:47 +0200 Subject: [PATCH 1/7] Initial --- src/Avalonia.Controls/Application.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index 6fdca557ebc..65191920e31 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -240,4 +240,11 @@ public virtual void RegisterServices() .Bind().ToTransient(); } } + + public enum ShutdownMode + { + OnLastWindowClose, + OnMainWindowClose, + OnExplicitShutdown + } } From 0ce8802e761005585433e32d9d4355a54b5d23ce Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Sat, 26 May 2018 19:53:32 +0200 Subject: [PATCH 2/7] Initial working state --- src/Avalonia.Controls/AppBuilderBase.cs | 45 +++--- src/Avalonia.Controls/Application.cs | 112 +++++++++++++-- src/Avalonia.Controls/ExitMode.cs | 12 ++ src/Avalonia.Controls/Window.cs | 45 +++--- src/Avalonia.Controls/WindowCollection.cs | 128 ++++++++++++++++++ .../WindowTests.cs | 13 +- 6 files changed, 299 insertions(+), 56 deletions(-) create mode 100644 src/Avalonia.Controls/ExitMode.cs create mode 100644 src/Avalonia.Controls/WindowCollection.cs diff --git a/src/Avalonia.Controls/AppBuilderBase.cs b/src/Avalonia.Controls/AppBuilderBase.cs index 7af3deef340..875f5263c27 100644 --- a/src/Avalonia.Controls/AppBuilderBase.cs +++ b/src/Avalonia.Controls/AppBuilderBase.cs @@ -15,7 +15,7 @@ namespace Avalonia.Controls public abstract class AppBuilderBase where TAppBuilder : AppBuilderBase, new() { private static bool s_setupWasAlreadyCalled; - + /// /// Gets or sets the instance. /// @@ -92,7 +92,7 @@ public static TAppBuilder Configure(Application app) }; } - protected TAppBuilder Self => (TAppBuilder) this; + protected TAppBuilder Self => (TAppBuilder)this; /// /// Registers a callback to call before Start is called on the . @@ -125,7 +125,6 @@ public void Start(Func dataContextProvider = null) var window = new TMainWindow(); if (dataContextProvider != null) window.DataContext = dataContextProvider(); - window.Show(); Instance.Run(window); } @@ -143,7 +142,6 @@ public void Start(TMainWindow mainWindow, Func dataContextP if (dataContextProvider != null) mainWindow.DataContext = dataContextProvider(); - mainWindow.Show(); Instance.Run(mainWindow); } @@ -209,6 +207,17 @@ static Action GetInitializer(string assemblyName) => () => public TAppBuilder UseAvaloniaModules() => AfterSetup(builder => SetupAvaloniaModules()); + /// + /// Sets the shutdown mode of the application. + /// + /// The shutdown mode. + /// + public TAppBuilder SetExitMode(ExitMode exitMode) + { + Instance.ExitMode = exitMode; + return Self; + } + private bool CheckSetup { get; set; } = true; /// @@ -223,20 +232,20 @@ internal TAppBuilder IgnoreSetupCheck() private void SetupAvaloniaModules() { var moduleInitializers = from assembly in AvaloniaLocator.Current.GetService().GetLoadedAssemblies() - from attribute in assembly.GetCustomAttributes() - where attribute.ForWindowingSubsystem == "" - || attribute.ForWindowingSubsystem == WindowingSubsystemName - where attribute.ForRenderingSubsystem == "" - || attribute.ForRenderingSubsystem == RenderingSubsystemName - group attribute by attribute.Name into exports - select (from export in exports - orderby export.ForWindowingSubsystem.Length descending - orderby export.ForRenderingSubsystem.Length descending - select export).First().ModuleType into moduleType - select (from constructor in moduleType.GetTypeInfo().DeclaredConstructors - where constructor.GetParameters().Length == 0 && !constructor.IsStatic - select constructor).Single() into constructor - select (Action)(() => constructor.Invoke(new object[0])); + from attribute in assembly.GetCustomAttributes() + where attribute.ForWindowingSubsystem == "" + || attribute.ForWindowingSubsystem == WindowingSubsystemName + where attribute.ForRenderingSubsystem == "" + || attribute.ForRenderingSubsystem == RenderingSubsystemName + group attribute by attribute.Name into exports + select (from export in exports + orderby export.ForWindowingSubsystem.Length descending + orderby export.ForRenderingSubsystem.Length descending + select export).First().ModuleType into moduleType + select (from constructor in moduleType.GetTypeInfo().DeclaredConstructors + where constructor.GetParameters().Length == 0 && !constructor.IsStatic + select constructor).Single() into constructor + select (Action)(() => constructor.Invoke(new object[0])); Delegate.Combine(moduleInitializers.ToArray()).DynamicInvoke(); } diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index 65191920e31..2a8eb22c3d9 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -43,11 +43,15 @@ public class Application : IApplicationLifecycle, IGlobalDataTemplates, IGlobalS private Styles _styles; private IResourceDictionary _resources; + private CancellationTokenSource _mainLoopCancellationTokenSource; + /// /// Initializes a new instance of the class. /// public Application() { + Windows = new WindowCollection(this); + OnExit += OnExiting; } @@ -158,6 +162,40 @@ public IResourceDictionary Resources /// IResourceNode IResourceNode.ResourceParent => null; + /// + /// Gets or sets the . This property indicates whether the application exits explicitly or implicitly. + /// If is set to OnExplicitExit the application is only closes if Exit is called. + /// The default is OnLastWindowClose + /// + /// + /// The shutdown mode. + /// + public ExitMode ExitMode { get; set; } + + /// + /// Gets or sets the main window of the application. + /// + /// + /// The main window. + /// + public Window MainWindow { get; set; } + + /// + /// Gets the open windows of the application. + /// + /// + /// The windows. + /// + public WindowCollection Windows { get; } + + /// + /// Gets or sets a value indicating whether this instance is existing. + /// + /// + /// true if this instance is existing; otherwise, false. + /// + internal bool IsExiting { get; set; } + /// /// Initializes the application by loading XAML etc. /// @@ -171,14 +209,58 @@ public virtual void Initialize() /// The closable to track public void Run(ICloseable closable) { - var source = new CancellationTokenSource(); - closable.Closed += OnExiting; - closable.Closed += (s, e) => source.Cancel(); - Dispatcher.UIThread.MainLoop(source.Token); + if (_mainLoopCancellationTokenSource != null) + { + throw new Exception("Run should only called once"); + } + + closable.Closed += (s, e) => Exit(); + + _mainLoopCancellationTokenSource = new CancellationTokenSource(); + + Dispatcher.UIThread.MainLoop(_mainLoopCancellationTokenSource.Token); + } + + /// + /// Runs the application's main loop until some condition occurs that is specified by ExitMode. + /// + /// The main window + public void Run(Window mainWindow) + { + if (_mainLoopCancellationTokenSource != null) + { + throw new Exception("Run should only called once"); + } + + _mainLoopCancellationTokenSource = new CancellationTokenSource(); + + Dispatcher.UIThread.InvokeAsync( + () => + { + if (mainWindow == null) + { + return; + } + + if (MainWindow != null) + { + return; + } + + if (!mainWindow.IsVisible) + { + mainWindow.Show(); + } + + MainWindow = mainWindow; + }, + DispatcherPriority.Send); + + Dispatcher.UIThread.MainLoop(_mainLoopCancellationTokenSource.Token); } - + /// - /// Runs the application's main loop until the is cancelled. + /// Runs the application's main loop until the is canceled. /// /// The token to track public void Run(CancellationToken token) @@ -191,7 +273,18 @@ public void Run(CancellationToken token) /// public void Exit() { + IsExiting = true; + + while (Windows.Count > 0) + { + Windows[0].Close(); + + Windows.RemoveAt(0); + } + OnExit?.Invoke(this, EventArgs.Empty); + + _mainLoopCancellationTokenSource?.Cancel(); } /// @@ -240,11 +333,4 @@ public virtual void RegisterServices() .Bind().ToTransient(); } } - - public enum ShutdownMode - { - OnLastWindowClose, - OnMainWindowClose, - OnExplicitShutdown - } } diff --git a/src/Avalonia.Controls/ExitMode.cs b/src/Avalonia.Controls/ExitMode.cs new file mode 100644 index 00000000000..0c5ecd7171d --- /dev/null +++ b/src/Avalonia.Controls/ExitMode.cs @@ -0,0 +1,12 @@ +// 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. + +namespace Avalonia +{ + public enum ExitMode + { + OnLastWindowClose, + OnMainWindowClose, + OnExplicitExit + } +} \ No newline at end of file diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index c4a09ac043d..71533029743 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -49,14 +49,6 @@ public enum SizeToContent /// public class Window : WindowBase, IStyleable, IFocusScope, ILayoutRoot, INameScope { - private static List s_windows = new List(); - - /// - /// Retrieves an enumeration of all Windows in the currently running application. - /// - public static IReadOnlyList OpenWindows => s_windows; - - /// /// Defines the property. /// public static readonly StyledProperty SizeToContentProperty = @@ -75,7 +67,7 @@ public class Window : WindowBase, IStyleable, IFocusScope, ILayoutRoot, INameSco AvaloniaProperty.Register(nameof(ShowInTaskbar), true); /// - /// Enables or disables the taskbar icon + /// Represents the current window state (normal, minimized, maximized) /// public static readonly StyledProperty WindowStateProperty = AvaloniaProperty.Register(nameof(WindowState)); @@ -117,7 +109,7 @@ static Window() BackgroundProperty.OverrideDefaultValue(typeof(Window), Brushes.White); TitleProperty.Changed.AddClassHandler((s, e) => s.PlatformImpl?.SetTitle((string)e.NewValue)); HasSystemDecorationsProperty.Changed.AddClassHandler( - (s, e) => s.PlatformImpl?.SetSystemDecorations((bool) e.NewValue)); + (s, e) => s.PlatformImpl?.SetSystemDecorations((bool)e.NewValue)); ShowInTaskbarProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.ShowTaskbarIcon((bool)e.NewValue)); @@ -151,7 +143,7 @@ public Window(IWindowImpl impl) if (PlatformImpl != null) PlatformImpl.WindowStateChanged = s => WindowState = s; } - + /// event EventHandler INameScope.Registered { @@ -201,7 +193,7 @@ public bool HasSystemDecorations get { return GetValue(HasSystemDecorationsProperty); } set { SetValue(HasSystemDecorationsProperty, value); } } - + /// /// Enables or disables the taskbar icon /// @@ -261,6 +253,20 @@ public WindowStartupLocation WindowStartupLocation /// public event EventHandler Closing; + internal static void AddWindow(Window window) + { + if (Application.Current == null) return; + + Application.Current.Windows.Add(window); + } + + internal static void RemoveWindow(Window window) + { + if (Application.Current == null) return; + + Application.Current.Windows.Remove(window); + } + /// /// Closes the window. /// @@ -300,10 +306,9 @@ internal void Close(bool ignoreCancel) finally { if (ignoreCancel || !cancelClosing) - { - s_windows.Remove(this); + { PlatformImpl?.Dispose(); - IsVisible = false; + HandleClosed(); } } } @@ -347,7 +352,7 @@ public override void Show() return; } - s_windows.Add(this); + AddWindow(this); EnsureInitialized(); SetWindowStartupLocation(); @@ -388,7 +393,7 @@ public Task ShowDialog() throw new InvalidOperationException("The window is already being shown."); } - s_windows.Add(this); + AddWindow(this); EnsureInitialized(); SetWindowStartupLocation(); @@ -397,7 +402,7 @@ public Task ShowDialog() using (BeginAutoSizing()) { - var affectedWindows = s_windows.Where(w => w.IsEnabled && w != this).ToList(); + var affectedWindows = Application.Current.Windows.Where(w => w.IsEnabled && w != this).ToList(); var activated = affectedWindows.Where(w => w.IsActive).FirstOrDefault(); SetIsEnabled(affectedWindows, false); @@ -501,8 +506,8 @@ protected override Size MeasureOverride(Size availableSize) protected override void HandleClosed() { - IsVisible = false; - s_windows.Remove(this); + RemoveWindow(this); + base.HandleClosed(); } diff --git a/src/Avalonia.Controls/WindowCollection.cs b/src/Avalonia.Controls/WindowCollection.cs new file mode 100644 index 00000000000..930d5ff30c6 --- /dev/null +++ b/src/Avalonia.Controls/WindowCollection.cs @@ -0,0 +1,128 @@ +// 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 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(); + } + + /// + /// Determines whether [contains] [the specified window]. + /// + /// The window. + /// + /// true if [contains] [the specified window]; otherwise, false. + /// + internal bool Contains(Window window) + { + return _windows.Contains(window); + } + + /// + /// Adds the specified window. + /// + /// The window. + internal void Add(Window window) + { + _windows.Add(window); + + window.Closed += OnWindowClosed; + } + + internal void Remove(Window window) + { + _windows.Remove(window); + } + + /// + /// Removes the window at a specific location. + /// + /// The index. + internal void RemoveAt(int index) + { + _windows.RemoveAt(index); + } + + private void OnWindowClosed(object sender, EventArgs eventArgs) + { + if (!(sender is Window window)) + { + return; + } + + if (_application.IsExiting) + { + return; + } + + switch (_application.ExitMode) + { + case ExitMode.OnLastWindowClose: + if (Count == 0) + { + _application.Exit(); + } + + break; + case ExitMode.OnMainWindowClose: + if (window == _application.MainWindow) + { + _application.Exit(); + } + + break; + + } + } + } +} \ No newline at end of file diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs index a85c4df8afe..f000db13661 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs @@ -129,7 +129,7 @@ public void Show_Should_Add_Window_To_OpenWindows() window.Show(); - Assert.Equal(new[] { window }, Window.OpenWindows); + Assert.Equal(new[] { window }, Application.Current.Windows); } } @@ -145,7 +145,7 @@ public void Window_Should_Be_Added_To_OpenWindows_Only_Once() window.Show(); window.IsVisible = true; - Assert.Equal(new[] { window }, Window.OpenWindows); + Assert.Equal(new[] { window }, Application.Current.Windows); window.Close(); } @@ -162,7 +162,7 @@ public void Close_Should_Remove_Window_From_OpenWindows() window.Show(); window.Close(); - Assert.Empty(Window.OpenWindows); + Assert.Empty(Application.Current.Windows); } } @@ -184,7 +184,7 @@ public void Impl_Closing_Should_Remove_Window_From_OpenWindows() window.Show(); windowImpl.Object.Closed(); - Assert.Empty(Window.OpenWindows); + Assert.Empty(Application.Current.Windows); } } @@ -339,7 +339,10 @@ private void ClearOpenWindows() { // HACK: We really need a decent way to have "statics" that can be scoped to // AvaloniaLocator scopes. - ((IList)Window.OpenWindows).Clear(); + while (Application.Current.Windows.Count > 0) + { + Application.Current.Windows[0].Close(); + } } } } From c51a14f08b7ee59923753e4785f81cdc321c0c48 Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Sat, 26 May 2018 20:15:48 +0200 Subject: [PATCH 3/7] Esure raise OnExit before terminating --- src/Avalonia.Controls/Application.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index 2a8eb22c3d9..4becdc22780 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -219,6 +219,11 @@ public void Run(ICloseable closable) _mainLoopCancellationTokenSource = new CancellationTokenSource(); Dispatcher.UIThread.MainLoop(_mainLoopCancellationTokenSource.Token); + + if (!IsExiting) + { + OnExit?.Invoke(this, EventArgs.Empty); + } } /// @@ -257,6 +262,11 @@ public void Run(Window mainWindow) DispatcherPriority.Send); Dispatcher.UIThread.MainLoop(_mainLoopCancellationTokenSource.Token); + + if (!IsExiting) + { + OnExit?.Invoke(this, EventArgs.Empty); + } } /// @@ -266,6 +276,11 @@ public void Run(Window mainWindow) public void Run(CancellationToken token) { Dispatcher.UIThread.MainLoop(token); + + if (!IsExiting) + { + OnExit?.Invoke(this, EventArgs.Empty); + } } /// From f4ddc2e94432dfd74b4a7dcc3ee64817b2c6cc5c Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Sat, 26 May 2018 20:23:05 +0200 Subject: [PATCH 4/7] Refactor OnExit --- src/Avalonia.Controls/Application.cs | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index 4becdc22780..fe81451be54 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -220,10 +220,7 @@ public void Run(ICloseable closable) Dispatcher.UIThread.MainLoop(_mainLoopCancellationTokenSource.Token); - if (!IsExiting) - { - OnExit?.Invoke(this, EventArgs.Empty); - } + OnExit?.Invoke(this, EventArgs.Empty); } /// @@ -237,7 +234,7 @@ public void Run(Window mainWindow) throw new Exception("Run should only called once"); } - _mainLoopCancellationTokenSource = new CancellationTokenSource(); + _mainLoopCancellationTokenSource = new CancellationTokenSource(); Dispatcher.UIThread.InvokeAsync( () => @@ -263,10 +260,7 @@ public void Run(Window mainWindow) Dispatcher.UIThread.MainLoop(_mainLoopCancellationTokenSource.Token); - if (!IsExiting) - { - OnExit?.Invoke(this, EventArgs.Empty); - } + OnExit?.Invoke(this, EventArgs.Empty); } /// @@ -277,10 +271,7 @@ public void Run(CancellationToken token) { Dispatcher.UIThread.MainLoop(token); - if (!IsExiting) - { - OnExit?.Invoke(this, EventArgs.Empty); - } + OnExit?.Invoke(this, EventArgs.Empty); } /// @@ -297,8 +288,6 @@ public void Exit() Windows.RemoveAt(0); } - OnExit?.Invoke(this, EventArgs.Empty); - _mainLoopCancellationTokenSource?.Cancel(); } From de71587bd77f2896b7efa45ec280a4dea1eb6b75 Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Sat, 26 May 2018 20:41:40 +0200 Subject: [PATCH 5/7] Revert delayed OnExit --- src/Avalonia.Controls/Application.cs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index fe81451be54..78eff006758 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -220,7 +220,10 @@ public void Run(ICloseable closable) Dispatcher.UIThread.MainLoop(_mainLoopCancellationTokenSource.Token); - OnExit?.Invoke(this, EventArgs.Empty); + if (!IsExiting) + { + OnExit?.Invoke(this, EventArgs.Empty); + } } /// @@ -260,7 +263,10 @@ public void Run(Window mainWindow) Dispatcher.UIThread.MainLoop(_mainLoopCancellationTokenSource.Token); - OnExit?.Invoke(this, EventArgs.Empty); + if (!IsExiting) + { + OnExit?.Invoke(this, EventArgs.Empty); + } } /// @@ -271,7 +277,10 @@ public void Run(CancellationToken token) { Dispatcher.UIThread.MainLoop(token); - OnExit?.Invoke(this, EventArgs.Empty); + if (!IsExiting) + { + OnExit?.Invoke(this, EventArgs.Empty); + } } /// @@ -288,6 +297,8 @@ public void Exit() Windows.RemoveAt(0); } + OnExit?.Invoke(this, EventArgs.Empty); + _mainLoopCancellationTokenSource?.Cancel(); } From 6156e9715c3886501c2df4997e49ef5bbab10bc2 Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Fri, 1 Jun 2018 12:27:00 +0200 Subject: [PATCH 6/7] OnMainWindowClose fix Added unit tests --- src/Avalonia.Controls/Application.cs | 2 - src/Avalonia.Controls/Window.cs | 14 ++- src/Avalonia.Controls/WindowCollection.cs | 36 +++----- .../ApplicationTests.cs | 89 +++++++++++++++++++ 4 files changed, 110 insertions(+), 31 deletions(-) create mode 100644 tests/Avalonia.Controls.UnitTests/ApplicationTests.cs diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index 78eff006758..9d8734308ef 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -293,8 +293,6 @@ public void Exit() while (Windows.Count > 0) { Windows[0].Close(); - - Windows.RemoveAt(0); } OnExit?.Invoke(this, EventArgs.Empty); diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 71533029743..8a5bd39f119 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -253,16 +253,22 @@ public WindowStartupLocation WindowStartupLocation /// public event EventHandler Closing; - internal static void AddWindow(Window window) + private static void AddWindow(Window window) { - if (Application.Current == null) return; + if (Application.Current == null) + { + return; + } Application.Current.Windows.Add(window); } - internal static void RemoveWindow(Window window) + private static void RemoveWindow(Window window) { - if (Application.Current == null) return; + if (Application.Current == null) + { + return; + } Application.Current.Windows.Remove(window); } diff --git a/src/Avalonia.Controls/WindowCollection.cs b/src/Avalonia.Controls/WindowCollection.cs index 930d5ff30c6..40f4297b285 100644 --- a/src/Avalonia.Controls/WindowCollection.cs +++ b/src/Avalonia.Controls/WindowCollection.cs @@ -19,21 +19,24 @@ 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. + /// Gets the at the specified index. /// /// - /// The . + /// The . /// /// The index. /// public Window this[int index] => _windows[index]; + /// /// /// Returns an enumerator that iterates through the collection. /// @@ -45,6 +48,7 @@ public IEnumerator GetEnumerator() return _windows.GetEnumerator(); } + /// /// /// Returns an enumerator that iterates through a collection. /// @@ -56,18 +60,6 @@ IEnumerator IEnumerable.GetEnumerator() return GetEnumerator(); } - /// - /// Determines whether [contains] [the specified window]. - /// - /// The window. - /// - /// true if [contains] [the specified window]; otherwise, false. - /// - internal bool Contains(Window window) - { - return _windows.Contains(window); - } - /// /// Adds the specified window. /// @@ -79,18 +71,13 @@ internal void Add(Window window) window.Closed += OnWindowClosed; } - internal void Remove(Window window) - { - _windows.Remove(window); - } - /// - /// Removes the window at a specific location. + /// Removes the specified window. /// - /// The index. - internal void RemoveAt(int index) + /// The window. + internal void Remove(Window window) { - _windows.RemoveAt(index); + _windows.Remove(window); } private void OnWindowClosed(object sender, EventArgs eventArgs) @@ -120,8 +107,7 @@ private void OnWindowClosed(object sender, EventArgs eventArgs) _application.Exit(); } - break; - + break; } } } diff --git a/tests/Avalonia.Controls.UnitTests/ApplicationTests.cs b/tests/Avalonia.Controls.UnitTests/ApplicationTests.cs new file mode 100644 index 00000000000..be9b2f2729a --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/ApplicationTests.cs @@ -0,0 +1,89 @@ +// 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. + +namespace Avalonia.Controls.UnitTests +{ + using Avalonia.UnitTests; + + using Xunit; + + public class ApplicationTests + { + [Fact] + public void Should_Exit_After_MainWindow_Closed() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + Application.Current.ExitMode = ExitMode.OnMainWindowClose; + + var mainWindow = new Window(); + + mainWindow.Show(); + + Application.Current.MainWindow = mainWindow; + + var window = new Window(); + + window.Show(); + + mainWindow.Close(); + + Assert.True(Application.Current.IsExiting); + } + } + + [Fact] + public void Should_Exit_After_Last_Window_Closed() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + Application.Current.ExitMode = ExitMode.OnLastWindowClose; + + var windowA = new Window(); + + windowA.Show(); + + var windowB = new Window(); + + windowB.Show(); + + windowA.Close(); + + Assert.False(Application.Current.IsExiting); + + windowB.Close(); + + Assert.True(Application.Current.IsExiting); + } + } + + [Fact] + public void Should_Only_Exit_On_Explicit_Exit() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + Application.Current.ExitMode = ExitMode.OnExplicitExit; + + var windowA = new Window(); + + windowA.Show(); + + var windowB = new Window(); + + windowB.Show(); + + windowA.Close(); + + Assert.False(Application.Current.IsExiting); + + windowB.Close(); + + Assert.False(Application.Current.IsExiting); + + Application.Current.Exit(); + + Assert.True(Application.Current.IsExiting); + } + } + } +} From 7ccfd5396f6a06e6d1661e1aa5f86559ed209646 Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Sun, 3 Jun 2018 17:38:52 +0200 Subject: [PATCH 7/7] Remove OnClose usage --- src/Avalonia.Controls/Application.cs | 8 ++--- src/Avalonia.Controls/WindowCollection.cs | 30 +++++++++++++++---- .../ApplicationTests.cs | 28 +++++++++++++---- .../WindowTests.cs | 5 +--- 4 files changed, 53 insertions(+), 18 deletions(-) diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index 9d8734308ef..ffe4a9c513c 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -220,6 +220,7 @@ public void Run(ICloseable closable) Dispatcher.UIThread.MainLoop(_mainLoopCancellationTokenSource.Token); + // Make sure we call OnExit in case an error happened and Exit() wasn't called explicitly if (!IsExiting) { OnExit?.Invoke(this, EventArgs.Empty); @@ -263,6 +264,7 @@ public void Run(Window mainWindow) Dispatcher.UIThread.MainLoop(_mainLoopCancellationTokenSource.Token); + // Make sure we call OnExit in case an error happened and Exit() wasn't called explicitly if (!IsExiting) { OnExit?.Invoke(this, EventArgs.Empty); @@ -277,6 +279,7 @@ public void Run(CancellationToken token) { Dispatcher.UIThread.MainLoop(token); + // Make sure we call OnExit in case an error happened and Exit() wasn't called explicitly if (!IsExiting) { OnExit?.Invoke(this, EventArgs.Empty); @@ -290,10 +293,7 @@ public void Exit() { IsExiting = true; - while (Windows.Count > 0) - { - Windows[0].Close(); - } + Windows.Clear(); OnExit?.Invoke(this, EventArgs.Empty); diff --git a/src/Avalonia.Controls/WindowCollection.cs b/src/Avalonia.Controls/WindowCollection.cs index 40f4297b285..c21a12f05b1 100644 --- a/src/Avalonia.Controls/WindowCollection.cs +++ b/src/Avalonia.Controls/WindowCollection.cs @@ -1,7 +1,6 @@ // 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; @@ -66,9 +65,12 @@ IEnumerator IEnumerable.GetEnumerator() /// The window. internal void Add(Window window) { - _windows.Add(window); + if (window == null) + { + return; + } - window.Closed += OnWindowClosed; + _windows.Add(window); } /// @@ -77,12 +79,30 @@ internal void Add(Window 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. + /// + internal void Clear() + { + while (_windows.Count > 0) + { + _windows[0].Close(); + } } - private void OnWindowClosed(object sender, EventArgs eventArgs) + private void OnRemoveWindow(Window window) { - if (!(sender is Window window)) + if (window == null) { return; } diff --git a/tests/Avalonia.Controls.UnitTests/ApplicationTests.cs b/tests/Avalonia.Controls.UnitTests/ApplicationTests.cs index be9b2f2729a..85f95b2b5c9 100644 --- a/tests/Avalonia.Controls.UnitTests/ApplicationTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ApplicationTests.cs @@ -1,12 +1,12 @@ // 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.Collections.Generic; +using Avalonia.UnitTests; +using Xunit; + namespace Avalonia.Controls.UnitTests { - using Avalonia.UnitTests; - - using Xunit; - public class ApplicationTests { [Fact] @@ -18,7 +18,7 @@ public void Should_Exit_After_MainWindow_Closed() var mainWindow = new Window(); - mainWindow.Show(); + mainWindow.Show(); Application.Current.MainWindow = mainWindow; @@ -85,5 +85,23 @@ public void Should_Only_Exit_On_Explicit_Exit() Assert.True(Application.Current.IsExiting); } } + + [Fact] + public void Should_Close_All_Remaining_Open_Windows_After_Explicit_Exit_Call() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var windows = new List { new Window(), new Window(), new Window(), new Window() }; + + foreach (var window in windows) + { + window.Show(); + } + + Application.Current.Exit(); + + Assert.Empty(Application.Current.Windows); + } + } } } diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs index f000db13661..e80ffd97cd8 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs @@ -339,10 +339,7 @@ private void ClearOpenWindows() { // HACK: We really need a decent way to have "statics" that can be scoped to // AvaloniaLocator scopes. - while (Application.Current.Windows.Count > 0) - { - Application.Current.Windows[0].Close(); - } + Application.Current.Windows.Clear(); } } }