Jelajahi Sumber

Merge pull request #1662 from Gillibald/feature/ApplicationExitMode

Application ExitMode
Steven Kirk 7 tahun lalu
induk
melakukan
4565aa8fd9

+ 27 - 18
src/Avalonia.Controls/AppBuilderBase.cs

@@ -15,7 +15,7 @@ namespace Avalonia.Controls
     public abstract class AppBuilderBase<TAppBuilder> where TAppBuilder : AppBuilderBase<TAppBuilder>, new()
     {
         private static bool s_setupWasAlreadyCalled;
-        
+
         /// <summary>
         /// Gets or sets the <see cref="IRuntimePlatform"/> instance.
         /// </summary>
@@ -92,7 +92,7 @@ namespace Avalonia.Controls
             };
         }
 
-        protected TAppBuilder Self => (TAppBuilder) this;
+        protected TAppBuilder Self => (TAppBuilder)this;
 
         /// <summary>
         /// Registers a callback to call before Start is called on the <see cref="Application"/>.
@@ -125,7 +125,6 @@ namespace Avalonia.Controls
             var window = new TMainWindow();
             if (dataContextProvider != null)
                 window.DataContext = dataContextProvider();
-            window.Show();
             Instance.Run(window);
         }
 
@@ -143,7 +142,6 @@ namespace Avalonia.Controls
 
             if (dataContextProvider != null)
                 mainWindow.DataContext = dataContextProvider();
-            mainWindow.Show();
             Instance.Run(mainWindow);
         }
 
@@ -209,25 +207,36 @@ namespace Avalonia.Controls
 
         public TAppBuilder UseAvaloniaModules() => AfterSetup(builder => SetupAvaloniaModules());
 
+        /// <summary>
+        /// Sets the shutdown mode of the application.
+        /// </summary>
+        /// <param name="exitMode">The shutdown mode.</param>
+        /// <returns></returns>
+        public TAppBuilder SetExitMode(ExitMode exitMode)
+        {
+            Instance.ExitMode = exitMode;
+            return Self;
+        }      
+
         protected virtual bool CheckSetup => true;
 
         private void SetupAvaloniaModules()
         {
             var moduleInitializers = from assembly in AvaloniaLocator.Current.GetService<IRuntimePlatform>().GetLoadedAssemblies()
-                                          from attribute in assembly.GetCustomAttributes<ExportAvaloniaModuleAttribute>()
-                                          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<ExportAvaloniaModuleAttribute>()
+                                     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();
         }
 

+ 105 - 6
src/Avalonia.Controls/Application.cs

@@ -43,11 +43,15 @@ namespace Avalonia
         private Styles _styles;
         private IResourceDictionary _resources;
 
+        private CancellationTokenSource _mainLoopCancellationTokenSource;
+
         /// <summary>
         /// Initializes a new instance of the <see cref="Application"/> class.
         /// </summary>
         public Application()
         {
+            Windows = new WindowCollection(this);
+
             OnExit += OnExiting;
         }
 
@@ -158,6 +162,40 @@ namespace Avalonia
         /// <inheritdoc/>
         IResourceNode IResourceNode.ResourceParent => null;
 
+        /// <summary>
+        /// Gets or sets the <see cref="ExitMode"/>. This property indicates whether the application exits explicitly or implicitly. 
+        /// If <see cref="ExitMode"/> is set to OnExplicitExit the application is only closes if Exit is called.
+        /// The default is OnLastWindowClose
+        /// </summary>
+        /// <value>
+        /// The shutdown mode.
+        /// </value>
+        public ExitMode ExitMode { get; set; }
+
+        /// <summary>
+        /// Gets or sets the main window of the application.
+        /// </summary>
+        /// <value>
+        /// The main window.
+        /// </value>
+        public Window MainWindow { get; set; }
+
+        /// <summary>
+        /// Gets the open windows of the application.
+        /// </summary>
+        /// <value>
+        /// The windows.
+        /// </value>
+        public WindowCollection Windows { get; }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether this instance is existing.
+        /// </summary>
+        /// <value>
+        ///   <c>true</c> if this instance is existing; otherwise, <c>false</c>.
+        /// </value>
+        internal bool IsExiting { get; set; }
+
         /// <summary>
         /// Initializes the application by loading XAML etc.
         /// </summary>
@@ -171,19 +209,74 @@ namespace Avalonia
         /// <param name="closable">The closable to track</param>
         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);
+
+            // Make sure we call OnExit in case an error happened and Exit() wasn't called explicitly
+            if (!IsExiting)
+            {
+                OnExit?.Invoke(this, EventArgs.Empty);
+            }
+        }
+
+        /// <summary>
+        /// Runs the application's main loop until some condition occurs that is specified by ExitMode.
+        /// </summary>
+        /// <param name="mainWindow">The main window</param>
+        public void Run(Window mainWindow)
+        {
+            if (_mainLoopCancellationTokenSource != null)
+            {
+                throw new Exception("Run should only called once");
+            }
+
+            _mainLoopCancellationTokenSource = new CancellationTokenSource();
+
+            if (MainWindow == null)
+            {
+                if (mainWindow == null)
+                {
+                    throw new ArgumentNullException(nameof(mainWindow));
+                }
+
+                if (!mainWindow.IsVisible)
+                {
+                    mainWindow.Show();
+                }
+
+                MainWindow = 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);
+            }
         }
-        
+
         /// <summary>
-        /// Runs the application's main loop until the <see cref="CancellationToken"/> is cancelled.
+        /// Runs the application's main loop until the <see cref="CancellationToken"/> is canceled.
         /// </summary>
         /// <param name="token">The token to track</param>
         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);
+            }
         }
 
         /// <summary>
@@ -191,7 +284,13 @@ namespace Avalonia
         /// </summary>
         public void Exit()
         {
+            IsExiting = true;
+
+            Windows.Clear();
+
             OnExit?.Invoke(this, EventArgs.Empty);
+
+            _mainLoopCancellationTokenSource?.Cancel();
         }
 
         /// <inheritdoc/>

+ 26 - 0
src/Avalonia.Controls/ExitMode.cs

@@ -0,0 +1,26 @@
+// 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
+{
+    /// <summary>
+    /// Enum for ExitMode
+    /// </summary>
+    public enum ExitMode
+    {
+        /// <summary>
+        /// Indicates an implicit call to Application.Exit when the last window closes.
+        /// </summary>
+        OnLastWindowClose,
+
+        /// <summary>
+        /// Indicates an implicit call to Application.Exit when the main window closes.
+        /// </summary>
+        OnMainWindowClose,
+
+        /// <summary>
+        /// Indicates that the application only exits on an explicit call to Application.Exit.
+        /// </summary>
+        OnExplicitExit
+    }
+}

+ 31 - 20
src/Avalonia.Controls/Window.cs

@@ -49,14 +49,6 @@ namespace Avalonia.Controls
     /// </summary>
     public class Window : WindowBase, IStyleable, IFocusScope, ILayoutRoot, INameScope
     {
-        private static List<Window> s_windows = new List<Window>();
-
-        /// <summary>
-        /// Retrieves an enumeration of all Windows in the currently running application.
-        /// </summary>
-        public static IReadOnlyList<Window> OpenWindows => s_windows;
-
-        /// <summary>
         /// Defines the <see cref="SizeToContent"/> property.
         /// </summary>
         public static readonly StyledProperty<SizeToContent> SizeToContentProperty =
@@ -75,7 +67,7 @@ namespace Avalonia.Controls
             AvaloniaProperty.Register<Window, bool>(nameof(ShowInTaskbar), true);
 
         /// <summary>
-        /// Enables or disables the taskbar icon
+        /// Represents the current window state (normal, minimized, maximized)
         /// </summary>
         public static readonly StyledProperty<WindowState> WindowStateProperty =
             AvaloniaProperty.Register<Window, WindowState>(nameof(WindowState));
@@ -117,7 +109,7 @@ namespace Avalonia.Controls
             BackgroundProperty.OverrideDefaultValue(typeof(Window), Brushes.White);
             TitleProperty.Changed.AddClassHandler<Window>((s, e) => s.PlatformImpl?.SetTitle((string)e.NewValue));
             HasSystemDecorationsProperty.Changed.AddClassHandler<Window>(
-                (s, e) => s.PlatformImpl?.SetSystemDecorations((bool) e.NewValue));
+                (s, e) => s.PlatformImpl?.SetSystemDecorations((bool)e.NewValue));
 
             ShowInTaskbarProperty.Changed.AddClassHandler<Window>((w, e) => w.PlatformImpl?.ShowTaskbarIcon((bool)e.NewValue));
 
@@ -149,7 +141,7 @@ namespace Avalonia.Controls
             _maxPlatformClientSize = PlatformImpl?.MaxClientSize ?? default(Size);
             Screens = new Screens(PlatformImpl?.Screen);
         }
-        
+
         /// <inheritdoc/>
         event EventHandler<NameScopeEventArgs> INameScope.Registered
         {
@@ -199,7 +191,7 @@ namespace Avalonia.Controls
             get { return GetValue(HasSystemDecorationsProperty); }
             set { SetValue(HasSystemDecorationsProperty, value); }
         }
-        
+
         /// <summary>
         /// Enables or disables the taskbar icon
         /// </summary>
@@ -259,6 +251,26 @@ namespace Avalonia.Controls
         /// </summary>
         public event EventHandler<CancelEventArgs> 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);
+        }
+
         /// <summary>
         /// Closes the window.
         /// </summary>
@@ -298,10 +310,9 @@ namespace Avalonia.Controls
             finally
             {
                 if (ignoreCancel || !cancelClosing)
-                {
-                    s_windows.Remove(this);
+                {                  
                     PlatformImpl?.Dispose();
-                    IsVisible = false;
+                    HandleClosed();
                 }
             }
         }
@@ -359,7 +370,7 @@ namespace Avalonia.Controls
                 return;
             }
 
-            s_windows.Add(this);
+            AddWindow(this);
 
             EnsureInitialized();
             SetWindowStartupLocation();
@@ -400,7 +411,7 @@ namespace Avalonia.Controls
                 throw new InvalidOperationException("The window is already being shown.");
             }
 
-            s_windows.Add(this);
+            AddWindow(this);
 
             EnsureInitialized();
             SetWindowStartupLocation();
@@ -409,7 +420,7 @@ namespace Avalonia.Controls
 
             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);
 
@@ -513,8 +524,8 @@ namespace Avalonia.Controls
 
         protected override void HandleClosed()
         {
-            IsVisible = false;
-            s_windows.Remove(this);
+            RemoveWindow(this);
+
             base.HandleClosed();
         }
 

+ 134 - 0
src/Avalonia.Controls/WindowCollection.cs

@@ -0,0 +1,134 @@
+// 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;
+using System.Collections.Generic;
+
+using Avalonia.Controls;
+
+namespace Avalonia
+{
+    public class WindowCollection : IReadOnlyList<Window>
+    {
+        private readonly Application _application;
+        private readonly List<Window> _windows = new List<Window>();
+
+        public WindowCollection(Application application)
+        {
+            _application = application;
+        }
+
+        /// <inheritdoc />
+        /// <summary>
+        /// Gets the number of elements in the collection.
+        /// </summary>
+        public int Count => _windows.Count;
+
+        /// <inheritdoc />
+        /// <summary>
+        /// Gets the <see cref="T:Avalonia.Controls.Window" /> at the specified index.
+        /// </summary>
+        /// <value>
+        /// The <see cref="T:Avalonia.Controls.Window" />.
+        /// </value>
+        /// <param name="index">The index.</param>
+        /// <returns></returns>
+        public Window this[int index] => _windows[index];
+
+        /// <inheritdoc />
+        /// <summary>
+        /// Returns an enumerator that iterates through the collection.
+        /// </summary>
+        /// <returns>
+        /// An enumerator that can be used to iterate through the collection.
+        /// </returns>
+        public IEnumerator<Window> GetEnumerator()
+        {
+            return _windows.GetEnumerator();
+        }
+
+        /// <inheritdoc />
+        /// <summary>
+        /// Returns an enumerator that iterates through a collection.
+        /// </summary>
+        /// <returns>
+        /// An <see cref="T:System.Collections.IEnumerator"></see> object that can be used to iterate through the collection.
+        /// </returns>
+        IEnumerator IEnumerable.GetEnumerator()
+        {
+            return GetEnumerator();
+        }
+
+        /// <summary>
+        /// Adds the specified window.
+        /// </summary>
+        /// <param name="window">The window.</param>
+        internal void Add(Window window)
+        {
+            if (window == null)
+            {
+                return;
+            }
+
+            _windows.Add(window);
+        }
+
+        /// <summary>
+        /// Removes the specified window.
+        /// </summary>
+        /// <param name="window">The window.</param>
+        internal void Remove(Window window)
+        {
+            if (window == null)
+            {
+                return;
+            }
+
+            _windows.Remove(window);
+
+            OnRemoveWindow(window);
+        }
+
+        /// <summary>
+        /// Closes all windows and removes them from the underlying collection.
+        /// </summary>
+        internal void Clear()
+        {
+            while (_windows.Count > 0)
+            {
+                _windows[0].Close();
+            }
+        }
+
+        private void OnRemoveWindow(Window window)
+        {
+            if (window == null)
+            {
+                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;                   
+            }
+        }
+    }
+}

+ 117 - 0
tests/Avalonia.Controls.UnitTests/ApplicationTests.cs

@@ -0,0 +1,117 @@
+// 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.Generic;
+using Avalonia.UnitTests;
+using Xunit;
+
+namespace Avalonia.Controls.UnitTests
+{
+    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);
+            }
+        }
+
+        [Fact]
+        public void Should_Close_All_Remaining_Open_Windows_After_Explicit_Exit_Call()
+        {
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            {
+                var windows = new List<Window> { new Window(), new Window(), new Window(), new Window() };
+
+                foreach (var window in windows)
+                {
+                    window.Show();
+                }
+
+                Application.Current.Exit();
+
+                Assert.Empty(Application.Current.Windows);
+            }
+        }
+
+        [Fact]
+        public void Throws_ArgumentNullException_On_Run_If_MainWindow_Is_Null()
+        {
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            {
+                Assert.Throws<ArgumentNullException>(() => { Application.Current.Run(null); });
+            }
+        }
+    }
+}

+ 5 - 5
tests/Avalonia.Controls.UnitTests/WindowTests.cs

@@ -129,7 +129,7 @@ namespace Avalonia.Controls.UnitTests
 
                 window.Show();
 
-                Assert.Equal(new[] { window }, Window.OpenWindows);
+                Assert.Equal(new[] { window }, Application.Current.Windows);
             }
         }
 
@@ -145,7 +145,7 @@ namespace Avalonia.Controls.UnitTests
                 window.Show();
                 window.IsVisible = true;
 
-                Assert.Equal(new[] { window }, Window.OpenWindows);
+                Assert.Equal(new[] { window }, Application.Current.Windows);
 
                 window.Close();
             }
@@ -162,7 +162,7 @@ namespace Avalonia.Controls.UnitTests
                 window.Show();
                 window.Close();
 
-                Assert.Empty(Window.OpenWindows);
+                Assert.Empty(Application.Current.Windows);
             }
         }
 
@@ -184,7 +184,7 @@ namespace Avalonia.Controls.UnitTests
                 window.Show();
                 windowImpl.Object.Closed();
 
-                Assert.Empty(Window.OpenWindows);
+                Assert.Empty(Application.Current.Windows);
             }
         }
 
@@ -339,7 +339,7 @@ namespace Avalonia.Controls.UnitTests
         {
             // HACK: We really need a decent way to have "statics" that can be scoped to
             // AvaloniaLocator scopes.
-            ((IList<Window>)Window.OpenWindows).Clear();
+            Application.Current.Windows.Clear();
         }
     }
 }