Browse Source

Merge pull request #2442 from Gillibald/feature/ApplicationOnStartup

Refactor Application.Run
pull/2528/head
Steven Kirk 7 years ago
committed by GitHub
parent
commit
3965cce37d
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 17
      src/Avalonia.Controls/AppBuilderBase.cs
  2. 203
      src/Avalonia.Controls/Application.cs
  3. 18
      src/Avalonia.Controls/ExitEventArgs.cs
  4. 12
      src/Avalonia.Controls/IApplicationLifecycle.cs
  5. 16
      src/Avalonia.Controls/ShutdownMode.cs
  6. 36
      src/Avalonia.Controls/StartupEventArgs.cs
  7. 4
      src/Avalonia.Controls/TopLevel.cs
  8. 4
      src/Avalonia.Controls/Window.cs
  9. 12
      src/Avalonia.Controls/WindowCollection.cs
  10. 48
      tests/Avalonia.Controls.UnitTests/ApplicationTests.cs
  11. 2
      tests/Avalonia.Controls.UnitTests/TopLevelTests.cs

17
src/Avalonia.Controls/AppBuilderBase.cs

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

203
src/Avalonia.Controls/Application.cs

@ -43,8 +43,8 @@ namespace Avalonia
private readonly Styler _styler = new Styler(); private readonly Styler _styler = new Styler();
private Styles _styles; private Styles _styles;
private IResourceDictionary _resources; private IResourceDictionary _resources;
private CancellationTokenSource _mainLoopCancellationTokenSource; private CancellationTokenSource _mainLoopCancellationTokenSource;
private int _exitCode;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="Application"/> class. /// Initializes a new instance of the <see cref="Application"/> class.
@ -52,10 +52,14 @@ namespace Avalonia
public Application() public Application()
{ {
Windows = new WindowCollection(this); Windows = new WindowCollection(this);
OnExit += OnExiting;
} }
/// <inheritdoc/>
public event EventHandler<StartupEventArgs> Startup;
/// <inheritdoc/>
public event EventHandler<ExitEventArgs> Exit;
/// <inheritdoc/> /// <inheritdoc/>
public event EventHandler<ResourcesChangedEventArgs> ResourcesChanged; public event EventHandler<ResourcesChangedEventArgs> ResourcesChanged;
@ -164,14 +168,14 @@ namespace Avalonia
IResourceNode IResourceNode.ResourceParent => null; IResourceNode IResourceNode.ResourceParent => null;
/// <summary> /// <summary>
/// Gets or sets the <see cref="ExitMode"/>. This property indicates whether the application exits explicitly or implicitly. /// Gets or sets the <see cref="ShutdownMode"/>. This property indicates whether the application is shutdown explicitly or implicitly.
/// If <see cref="ExitMode"/> is set to OnExplicitExit the application is only closes if Exit is called. /// If <see cref="ShutdownMode"/> is set to OnExplicitShutdown the application is only closes if Shutdown is called.
/// The default is OnLastWindowClose /// The default is OnLastWindowClose
/// </summary> /// </summary>
/// <value> /// <value>
/// The shutdown mode. /// The shutdown mode.
/// </value> /// </value>
public ExitMode ExitMode { get; set; } public ShutdownMode ShutdownMode { get; set; }
/// <summary> /// <summary>
/// Gets or sets the main window of the application. /// Gets or sets the main window of the application.
@ -190,108 +194,171 @@ namespace Avalonia
public WindowCollection Windows { get; } public WindowCollection Windows { get; }
/// <summary> /// <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> /// </summary>
/// <value> /// <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> /// </value>
internal bool IsExiting { get; set; } internal bool IsShuttingDown { get; private set; }
/// <summary> /// <summary>
/// Initializes the application by loading XAML etc. /// Initializes the application by loading XAML etc.
/// </summary> /// </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> /// <summary>
/// Runs the application's main loop until the <see cref="ICloseable"/> is closed. /// Runs the application's main loop.
/// </summary> /// </summary>
/// <param name="closable">The closable to track</param> /// <remarks>
public void Run(ICloseable closable) /// 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(); if (MainWindow == null)
{
_mainLoopCancellationTokenSource = new CancellationTokenSource(); 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 return Run(new CancellationTokenSource());
if (!IsExiting)
{
OnExit?.Invoke(this, EventArgs.Empty);
}
} }
/// <summary> /// <summary>
/// Runs the application's main loop until some condition occurs that is specified by ExitMode. /// Runs the application's main loop.
/// </summary> /// </summary>
/// <param name="mainWindow">The main window</param> /// <remarks>
public void Run(Window mainWindow) /// 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 (_mainLoopCancellationTokenSource != null)
if (MainWindow == null)
{ {
if (mainWindow == null) throw new InvalidOperationException("Application is already running.");
{ }
throw new ArgumentNullException(nameof(mainWindow));
}
if (!mainWindow.IsVisible) _mainLoopCancellationTokenSource = tokenSource;
{
mainWindow.Show();
}
MainWindow = mainWindow; Dispatcher.UIThread.Post(() => OnStartup(new StartupEventArgs()), DispatcherPriority.Send);
}
Dispatcher.UIThread.MainLoop(_mainLoopCancellationTokenSource.Token); Dispatcher.UIThread.MainLoop(_mainLoopCancellationTokenSource.Token);
// Make sure we call OnExit in case an error happened and Exit() wasn't called explicitly if (!IsShuttingDown)
if (!IsExiting)
{ {
OnExit?.Invoke(this, EventArgs.Empty); Shutdown(_exitCode);
} }
return _exitCode;
} }
/// <summary> /// <summary>
/// Runs the application's main loop until the <see cref="CancellationToken"/> is canceled. /// Raises the <see cref="Startup"/> event.
/// </summary> /// </summary>
/// <param name="token">The token to track</param> /// <param name="e">A <see cref="StartupEventArgs"/> that contains the event data.</param>
public void Run(CancellationToken token) protected virtual void OnStartup(StartupEventArgs e)
{ {
Dispatcher.UIThread.MainLoop(token); Startup?.Invoke(this, e);
// Make sure we call OnExit in case an error happened and Exit() wasn't called explicitly
if (!IsExiting)
{
OnExit?.Invoke(this, EventArgs.Empty);
}
} }
/// <summary> /// <summary>
/// Exits the application /// Raises the <see cref="Exit"/> event.
/// </summary> /// </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(); 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/> /// <inheritdoc/>
@ -302,20 +369,6 @@ namespace Avalonia
Styles.TryGetResource(key, out value); 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> /// <summary>
/// Register's the services needed by Avalonia. /// Register's the services needed by Avalonia.
/// </summary> /// </summary>

18
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; }
}
}

12
src/Avalonia.Controls/IApplicationLifecycle.cs

@ -7,14 +7,20 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
public interface IApplicationLifecycle public interface IApplicationLifecycle
{ {
/// <summary>
/// Sent when the application is starting up.
/// </summary>
event EventHandler<StartupEventArgs> Startup;
/// <summary> /// <summary>
/// Sent when the application is exiting. /// Sent when the application is exiting.
/// </summary> /// </summary>
event EventHandler OnExit; event EventHandler<ExitEventArgs> Exit;
/// <summary> /// <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> /// </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);
} }
} }

16
src/Avalonia.Controls/ExitMode.cs → src/Avalonia.Controls/ShutdownMode.cs

@ -1,26 +1,26 @@
// Copyright (c) The Avalonia Project. All rights reserved. // 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. // Licensed under the MIT license. See licence.md file in the project root for full license information.
namespace Avalonia namespace Avalonia.Controls
{ {
/// <summary> /// <summary>
/// Enum for ExitMode /// Describes the possible values for <see cref="Application.ShutdownMode"/>.
/// </summary> /// </summary>
public enum ExitMode public enum ShutdownMode
{ {
/// <summary> /// <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> /// </summary>
OnLastWindowClose, OnLastWindowClose,
/// <summary> /// <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> /// </summary>
OnMainWindowClose, OnMainWindowClose,
/// <summary> /// <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> /// </summary>
OnExplicitExit OnExplicitShutdown
} }
} }

36
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];
}
}
}
}

4
src/Avalonia.Controls/TopLevel.cs

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

4
src/Avalonia.Controls/Window.cs

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

12
src/Avalonia.Controls/WindowCollection.cs

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

48
tests/Avalonia.Controls.UnitTests/ApplicationTests.cs

@ -3,6 +3,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Avalonia.Threading;
using Avalonia.UnitTests; using Avalonia.UnitTests;
using Xunit; using Xunit;
@ -15,7 +16,11 @@ namespace Avalonia.Controls.UnitTests
{ {
using (UnitTestApplication.Start(TestServices.StyledWindow)) 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(); var mainWindow = new Window();
@ -29,7 +34,7 @@ namespace Avalonia.Controls.UnitTests
mainWindow.Close(); mainWindow.Close();
Assert.True(Application.Current.IsExiting); Assert.True(hasExit);
} }
} }
@ -38,7 +43,11 @@ namespace Avalonia.Controls.UnitTests
{ {
using (UnitTestApplication.Start(TestServices.StyledWindow)) 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(); var windowA = new Window();
@ -50,11 +59,11 @@ namespace Avalonia.Controls.UnitTests
windowA.Close(); windowA.Close();
Assert.False(Application.Current.IsExiting); Assert.False(hasExit);
windowB.Close(); windowB.Close();
Assert.True(Application.Current.IsExiting); Assert.True(hasExit);
} }
} }
@ -63,7 +72,11 @@ namespace Avalonia.Controls.UnitTests
{ {
using (UnitTestApplication.Start(TestServices.StyledWindow)) 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(); var windowA = new Window();
@ -75,15 +88,15 @@ namespace Avalonia.Controls.UnitTests
windowA.Close(); windowA.Close();
Assert.False(Application.Current.IsExiting); Assert.False(hasExit);
windowB.Close(); 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(); window.Show();
} }
Application.Current.Exit(); Application.Current.Shutdown();
Assert.Empty(Application.Current.Windows); Assert.Empty(Application.Current.Windows);
} }
@ -129,5 +142,18 @@ namespace Avalonia.Controls.UnitTests
Assert.True(raised); 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);
}
}
} }
} }

2
tests/Avalonia.Controls.UnitTests/TopLevelTests.cs

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

Loading…
Cancel
Save