Browse Source

Merge pull request #2442 from Gillibald/feature/ApplicationOnStartup

Refactor Application.Run
Steven Kirk 6 years ago
parent
commit
3965cce37d

+ 12 - 5
src/Avalonia.Controls/AppBuilderBase.cs

@@ -148,13 +148,20 @@ namespace Avalonia.Controls
 
         public delegate void AppMainDelegate(Application app, string[] args);
 
+        public void Start()
+        {
+            Setup();
+            BeforeStartCallback(Self);
+            Instance.Run();
+        }
+
         public void Start(AppMainDelegate main, string[] args)
         {
             Setup();
             BeforeStartCallback(Self);
             main(Instance, args);
         }
-        
+
         /// <summary>
         /// Sets up the platform-specific services for the application, but does not run it.
         /// </summary>
@@ -220,13 +227,13 @@ namespace Avalonia.Controls
         /// <summary>
         /// Sets the shutdown mode of the application.
         /// </summary>
-        /// <param name="exitMode">The shutdown mode.</param>
+        /// <param name="shutdownMode">The shutdown mode.</param>
         /// <returns></returns>
-        public TAppBuilder SetExitMode(ExitMode exitMode)
+        public TAppBuilder SetShutdownMode(ShutdownMode shutdownMode)
         {
-            Instance.ExitMode = exitMode;
+            Instance.ShutdownMode = shutdownMode;
             return Self;
-        }      
+        }
 
         protected virtual bool CheckSetup => true;
 

+ 128 - 75
src/Avalonia.Controls/Application.cs

@@ -43,8 +43,8 @@ namespace Avalonia
         private readonly Styler _styler = new Styler();
         private Styles _styles;
         private IResourceDictionary _resources;
-
         private CancellationTokenSource _mainLoopCancellationTokenSource;
+        private int _exitCode;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="Application"/> class.
@@ -52,10 +52,14 @@ namespace Avalonia
         public Application()
         {
             Windows = new WindowCollection(this);
-
-            OnExit += OnExiting;
         }
 
+        /// <inheritdoc/>
+        public event EventHandler<StartupEventArgs> Startup;
+
+        /// <inheritdoc/>
+        public event EventHandler<ExitEventArgs> Exit;
+
         /// <inheritdoc/>
         public event EventHandler<ResourcesChangedEventArgs> ResourcesChanged;
 
@@ -164,14 +168,14 @@ namespace Avalonia
         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.
+        /// Gets or sets the <see cref="ShutdownMode"/>. This property indicates whether the application is shutdown explicitly or implicitly. 
+        /// If <see cref="ShutdownMode"/> is set to OnExplicitShutdown the application is only closes if Shutdown is called.
         /// The default is OnLastWindowClose
         /// </summary>
         /// <value>
         /// The shutdown mode.
         /// </value>
-        public ExitMode ExitMode { get; set; }
+        public ShutdownMode ShutdownMode { get; set; }
 
         /// <summary>
         /// Gets or sets the main window of the application.
@@ -190,108 +194,171 @@ namespace Avalonia
         public WindowCollection Windows { get; }
 
         /// <summary>
-        /// Gets or sets a value indicating whether this instance is existing.
+        /// Gets or sets a value indicating whether this instance is shutting down.
         /// </summary>
         /// <value>
-        ///   <c>true</c> if this instance is existing; otherwise, <c>false</c>.
+        ///   <c>true</c> if this instance is shutting down; otherwise, <c>false</c>.
         /// </value>
-        internal bool IsExiting { get; set; }
+        internal bool IsShuttingDown { get; private set; }
 
         /// <summary>
         /// Initializes the application by loading XAML etc.
         /// </summary>
-        public virtual void Initialize()
+        public virtual void Initialize() { }
+
+        /// <summary>
+        /// Runs the application's main loop.
+        /// </summary>
+        /// <remarks>
+        /// This will return when the <see cref="Avalonia.Controls.ShutdownMode"/> condition is met
+        /// or <see cref="Shutdown(int)"/> was called. 
+        /// </remarks>
+        /// <returns>The application's exit code that is returned to the operating system on termination.</returns>
+        public int Run()
         {
+            return Run(new CancellationTokenSource());
         }
 
         /// <summary>
-        /// Runs the application's main loop until the <see cref="ICloseable"/> is closed.
+        /// Runs the application's main loop.
         /// </summary>
-        /// <param name="closable">The closable to track</param>
-        public void Run(ICloseable closable)
+        /// <remarks>
+        /// This will return when the <see cref="Avalonia.Controls.ShutdownMode"/> condition is met
+        /// or <see cref="Shutdown(int)"/> was called.
+        /// This also returns when <see cref="ICloseable"/> is closed.
+        /// </remarks>
+        /// <param name="closable">The closable to track.</param>
+        /// <returns>The application's exit code that is returned to the operating system on termination.</returns>
+        public int Run(ICloseable closable)
         {
-            if (_mainLoopCancellationTokenSource != null)
+            closable.Closed += (s, e) => _mainLoopCancellationTokenSource?.Cancel();
+
+            return Run(new CancellationTokenSource());
+        }
+
+        /// <summary>
+        /// Runs the application's main loop.
+        /// </summary>
+        /// <remarks>
+        /// This will return when the <see cref="Avalonia.Controls.ShutdownMode"/> condition is met
+        /// or <see cref="Shutdown(int)"/> was called.
+        /// </remarks>
+        /// <param name="mainWindow">The window that is used as <see cref="MainWindow"/>
+        /// when the <see cref="MainWindow"/> isn't already set.</param>
+        /// <returns>The application's exit code that is returned to the operating system on termination.</returns>
+        public int Run(Window mainWindow)
+        {
+            if (mainWindow == null)
             {
-                throw new Exception("Run should only called once");
+                throw new ArgumentNullException(nameof(mainWindow));
             }
 
-            closable.Closed += (s, e) => Exit();
-
-            _mainLoopCancellationTokenSource = new CancellationTokenSource();
+            if (MainWindow == null)
+            {
+                Dispatcher.UIThread.Post(() =>
+                {
+                    if (!mainWindow.IsVisible)
+                    {
+                        mainWindow.Show();
+                    }
 
-            Dispatcher.UIThread.MainLoop(_mainLoopCancellationTokenSource.Token);
+                    MainWindow = mainWindow;
+                });
+            }            
 
-            // Make sure we call OnExit in case an error happened and Exit() wasn't called explicitly
-            if (!IsExiting)
-            {
-                OnExit?.Invoke(this, EventArgs.Empty);
-            }
+            return Run(new CancellationTokenSource());
         }
-
         /// <summary>
-        /// Runs the application's main loop until some condition occurs that is specified by ExitMode.
+        /// Runs the application's main loop.
         /// </summary>
-        /// <param name="mainWindow">The main window</param>
-        public void Run(Window mainWindow)
+        /// <remarks>
+        /// This will return when the <see cref="Avalonia.Controls.ShutdownMode"/> condition is met
+        /// or <see cref="Shutdown(int)"/> was called.
+        /// This also returns when the <see cref="CancellationToken"/> is canceled.
+        /// </remarks>
+        /// <returns>The application's exit code that is returned to the operating system on termination.</returns>
+        /// <param name="token">The token to track.</param>
+        public int Run(CancellationToken token)
         {
-            if (_mainLoopCancellationTokenSource != null)
+            return Run(CancellationTokenSource.CreateLinkedTokenSource(token));
+        }
+
+        private int Run(CancellationTokenSource tokenSource)
+        {
+            if (IsShuttingDown)
             {
-                throw new Exception("Run should only called once");
+                throw new InvalidOperationException("Application is shutting down.");
             }
 
-            _mainLoopCancellationTokenSource = new CancellationTokenSource();
-
-            if (MainWindow == null)
+            if (_mainLoopCancellationTokenSource != null)
             {
-                if (mainWindow == null)
-                {
-                    throw new ArgumentNullException(nameof(mainWindow));
-                }
+                throw new InvalidOperationException("Application is already running.");
+            }
 
-                if (!mainWindow.IsVisible)
-                {
-                    mainWindow.Show();
-                }
+            _mainLoopCancellationTokenSource = tokenSource;
 
-                MainWindow = mainWindow;
-            }           
+            Dispatcher.UIThread.Post(() => OnStartup(new StartupEventArgs()), DispatcherPriority.Send);
 
             Dispatcher.UIThread.MainLoop(_mainLoopCancellationTokenSource.Token);
 
-            // Make sure we call OnExit in case an error happened and Exit() wasn't called explicitly
-            if (!IsExiting)
+            if (!IsShuttingDown)
             {
-                OnExit?.Invoke(this, EventArgs.Empty);
+                Shutdown(_exitCode);
             }
+
+            return _exitCode;
         }
 
         /// <summary>
-        /// Runs the application's main loop until the <see cref="CancellationToken"/> is canceled.
+        /// Raises the <see cref="Startup"/> event.
         /// </summary>
-        /// <param name="token">The token to track</param>
-        public void Run(CancellationToken token)
+        /// <param name="e">A <see cref="StartupEventArgs"/> that contains the event data.</param>
+        protected virtual void OnStartup(StartupEventArgs e)
         {
-            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);
-            }
+            Startup?.Invoke(this, e);
         }
 
         /// <summary>
-        /// Exits the application
+        /// Raises the <see cref="Exit"/> event.
         /// </summary>
-        public void Exit()
+        /// <param name="e">A <see cref="ExitEventArgs"/> that contains the event data.</param>
+        protected virtual void OnExit(ExitEventArgs e)
         {
-            IsExiting = true;
+            Exit?.Invoke(this, e);
+        }
+
+        /// <inheritdoc/>
+        public void Shutdown(int exitCode = 0)
+        {
+            if (IsShuttingDown)
+            {
+                throw new InvalidOperationException("Application is already shutting down.");
+            }
+
+            _exitCode = exitCode;
+
+            IsShuttingDown = true;         
 
             Windows.Clear();
 
-            OnExit?.Invoke(this, EventArgs.Empty);
+            try
+            {
+                var e = new ExitEventArgs { ApplicationExitCode = _exitCode };
+
+                OnExit(e);
+
+                _exitCode = e.ApplicationExitCode;                
+            }
+            finally
+            {
+                _mainLoopCancellationTokenSource?.Cancel();
+
+                _mainLoopCancellationTokenSource = null;
+
+                IsShuttingDown = false;
 
-            _mainLoopCancellationTokenSource?.Cancel();
+                Environment.ExitCode = _exitCode;
+            }
         }
 
         /// <inheritdoc/>
@@ -302,20 +369,6 @@ namespace Avalonia
                    Styles.TryGetResource(key, out value);
         }
 
-        /// <summary>
-        /// Sent when the application is exiting.
-        /// </summary>
-        public event EventHandler OnExit;
-
-        /// <summary>
-        /// Called when the application is exiting.
-        /// </summary>
-        /// <param name="sender"></param>
-        /// <param name="e"></param>
-        protected virtual void OnExiting(object sender, EventArgs e)
-        {
-        }
-
         /// <summary>
         /// Register's the services needed by Avalonia.
         /// </summary>

+ 18 - 0
src/Avalonia.Controls/ExitEventArgs.cs

@@ -0,0 +1,18 @@
+// 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;
+
+namespace Avalonia.Controls
+{
+    /// <summary>
+    /// Contains the arguments for the <see cref="IApplicationLifecycle.Exit"/> event.
+    /// </summary>
+    public class ExitEventArgs : EventArgs
+    {
+        /// <summary>
+        /// Gets or sets the exit code that an application returns to the operating system when the application exits.
+        /// </summary>
+        public int ApplicationExitCode { get; set; }
+    }
+}

+ 9 - 3
src/Avalonia.Controls/IApplicationLifecycle.cs

@@ -7,14 +7,20 @@ namespace Avalonia.Controls
     /// </summary>
     public interface IApplicationLifecycle
     {
+        /// <summary>
+        /// Sent when the application is starting up.
+        /// </summary>
+        event EventHandler<StartupEventArgs> Startup;
+
         /// <summary>
         /// Sent when the application is exiting.
         /// </summary>
-        event EventHandler OnExit;
+        event EventHandler<ExitEventArgs> Exit;
 
         /// <summary>
-        /// Exits the application.
+        /// Shuts down the application and sets the exit code that is returned to the operating system when the application exits.
         /// </summary>
-        void Exit();
+        /// <param name="exitCode">An integer exit code for an application. The default exit code is 0.</param>
+        void Shutdown(int exitCode = 0);
     }
 }

+ 8 - 8
src/Avalonia.Controls/ExitMode.cs → src/Avalonia.Controls/ShutdownMode.cs

@@ -1,26 +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
+namespace Avalonia.Controls
 {
     /// <summary>
-    /// Enum for ExitMode
+    /// Describes the possible values for <see cref="Application.ShutdownMode"/>.
     /// </summary>
-    public enum ExitMode
+    public enum ShutdownMode
     {
         /// <summary>
-        /// Indicates an implicit call to Application.Exit when the last window closes.
+        /// Indicates an implicit call to Application.Shutdown when the last window closes.
         /// </summary>
         OnLastWindowClose,
 
         /// <summary>
-        /// Indicates an implicit call to Application.Exit when the main window closes.
+        /// Indicates an implicit call to Application.Shutdown when the main window closes.
         /// </summary>
         OnMainWindowClose,
 
         /// <summary>
-        /// Indicates that the application only exits on an explicit call to Application.Exit.
+        /// Indicates that the application only exits on an explicit call to Application.Shutdown.
         /// </summary>
-        OnExplicitExit
+        OnExplicitShutdown
     }
-}
+}

+ 36 - 0
src/Avalonia.Controls/StartupEventArgs.cs

@@ -0,0 +1,36 @@
+// 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 System.Linq;
+
+namespace Avalonia.Controls
+{
+    /// <summary>
+    /// Contains the arguments for the <see cref="IApplicationLifecycle.Startup"/> event.
+    /// </summary>
+    public class StartupEventArgs : EventArgs
+    {
+        private string[] _args;
+
+        /// <summary>
+        /// Gets the command line arguments that were passed to the application.
+        /// </summary>
+        public IReadOnlyList<string> Args => _args ?? (_args = GetArgs());
+
+        private static string[] GetArgs()
+        {
+            try
+            {
+                var args = Environment.GetCommandLineArgs();
+
+                return args.Length > 1 ? args.Skip(1).ToArray() : new string[0];
+            }
+            catch (NotSupportedException)
+            {
+                return new string[0];
+            }
+        }
+    }
+}

+ 2 - 2
src/Avalonia.Controls/TopLevel.cs

@@ -127,7 +127,7 @@ namespace Avalonia.Controls
 
             if (_applicationLifecycle != null)
             {
-                _applicationLifecycle.OnExit += OnApplicationExiting;
+                _applicationLifecycle.Exit += OnApplicationExiting;
             }
 
             if (((IStyleHost)this).StylingParent is IResourceProvider applicationResources)
@@ -281,7 +281,7 @@ namespace Avalonia.Controls
             Closed?.Invoke(this, EventArgs.Empty);
             Renderer?.Dispose();
             Renderer = null;
-            _applicationLifecycle.OnExit -= OnApplicationExiting;
+            _applicationLifecycle.Exit -= OnApplicationExiting;
         }
 
         /// <summary>

+ 2 - 2
src/Avalonia.Controls/Window.cs

@@ -250,7 +250,7 @@ namespace Avalonia.Controls
         /// <summary>
         /// Fired before a window is closed.
         /// </summary>
-        public event EventHandler<CancelEventArgs> Closing;
+        public event EventHandler<CancelEventArgs> Closing;      
 
         private static void AddWindow(Window window)
         {
@@ -440,7 +440,7 @@ namespace Avalonia.Controls
         /// </returns>
         public Task<TResult> ShowDialog<TResult>(IWindowImpl owner)
         {
-            if(owner == null)
+            if (owner == null)
                 throw new ArgumentNullException(nameof(owner));
 
             if (IsVisible)

+ 6 - 6
src/Avalonia.Controls/WindowCollection.cs

@@ -107,24 +107,24 @@ namespace Avalonia
                 return;
             }
 
-            if (_application.IsExiting)
+            if (_application.IsShuttingDown)
             {
                 return;
             }
 
-            switch (_application.ExitMode)
+            switch (_application.ShutdownMode)
             {
-                case ExitMode.OnLastWindowClose:
+                case ShutdownMode.OnLastWindowClose:
                     if (Count == 0)
                     {
-                        _application.Exit();
+                        _application.Shutdown();
                     }
 
                     break;
-                case ExitMode.OnMainWindowClose:
+                case ShutdownMode.OnMainWindowClose:
                     if (window == _application.MainWindow)
                     {
-                        _application.Exit();
+                        _application.Shutdown();
                     }
 
                     break;                   

+ 37 - 11
tests/Avalonia.Controls.UnitTests/ApplicationTests.cs

@@ -3,6 +3,7 @@
 
 using System;
 using System.Collections.Generic;
+using Avalonia.Threading;
 using Avalonia.UnitTests;
 using Xunit;
 
@@ -15,7 +16,11 @@ namespace Avalonia.Controls.UnitTests
         {
             using (UnitTestApplication.Start(TestServices.StyledWindow))
             {
-                Application.Current.ExitMode = ExitMode.OnMainWindowClose;
+                Application.Current.ShutdownMode = ShutdownMode.OnMainWindowClose;
+
+                var hasExit = false;
+
+                Application.Current.Exit += (s, e) => hasExit = true;
 
                 var mainWindow = new Window();
 
@@ -29,7 +34,7 @@ namespace Avalonia.Controls.UnitTests
 
                 mainWindow.Close();
 
-                Assert.True(Application.Current.IsExiting);
+                Assert.True(hasExit);
             }
         }
 
@@ -38,7 +43,11 @@ namespace Avalonia.Controls.UnitTests
         {
             using (UnitTestApplication.Start(TestServices.StyledWindow))
             {
-                Application.Current.ExitMode = ExitMode.OnLastWindowClose;
+                Application.Current.ShutdownMode = ShutdownMode.OnLastWindowClose;
+
+                var hasExit = false;
+
+                Application.Current.Exit += (s, e) => hasExit = true;
 
                 var windowA = new Window();
 
@@ -50,11 +59,11 @@ namespace Avalonia.Controls.UnitTests
 
                 windowA.Close();
 
-                Assert.False(Application.Current.IsExiting);
+                Assert.False(hasExit);
 
                 windowB.Close();
 
-                Assert.True(Application.Current.IsExiting);
+                Assert.True(hasExit);
             }
         }
 
@@ -63,7 +72,11 @@ namespace Avalonia.Controls.UnitTests
         {
             using (UnitTestApplication.Start(TestServices.StyledWindow))
             {
-                Application.Current.ExitMode = ExitMode.OnExplicitExit;
+                Application.Current.ShutdownMode = ShutdownMode.OnExplicitShutdown;
+
+                var hasExit = false;
+
+                Application.Current.Exit += (s, e) => hasExit = true;
 
                 var windowA = new Window();
 
@@ -75,15 +88,15 @@ namespace Avalonia.Controls.UnitTests
 
                 windowA.Close();
 
-                Assert.False(Application.Current.IsExiting);
+                Assert.False(hasExit);
 
                 windowB.Close();
 
-                Assert.False(Application.Current.IsExiting);
+                Assert.False(hasExit);
 
-                Application.Current.Exit();
+                Application.Current.Shutdown();
 
-                Assert.True(Application.Current.IsExiting);
+                Assert.True(hasExit);
             }
         }
 
@@ -99,7 +112,7 @@ namespace Avalonia.Controls.UnitTests
                     window.Show();
                 }
 
-                Application.Current.Exit();
+                Application.Current.Shutdown();
 
                 Assert.Empty(Application.Current.Windows);
             }
@@ -129,5 +142,18 @@ namespace Avalonia.Controls.UnitTests
                 Assert.True(raised);
             }
         }
+
+        [Fact]
+        public void Should_Set_ExitCode_After_Shutdown()
+        {
+            using (UnitTestApplication.Start(TestServices.MockThreadingInterface))
+            {
+                Application.Current.Shutdown(1337);
+
+                var exitCode = Application.Current.Run();
+
+                Assert.Equal(1337, exitCode);
+            }
+        }
     }
 }

+ 1 - 1
tests/Avalonia.Controls.UnitTests/TopLevelTests.cs

@@ -215,7 +215,7 @@ namespace Avalonia.Controls.UnitTests
                 var impl = new Mock<ITopLevelImpl>();
                 impl.SetupAllProperties();
                 var target = new TestTopLevel(impl.Object);
-                UnitTestApplication.Current.Exit();
+                UnitTestApplication.Current.Shutdown();
                 Assert.True(target.IsClosed);
             }
         }