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

203
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
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>
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);
}
}

16
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
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)
{
_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>

4
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)

12
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;

48
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);
}
}
}
}

2
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);
}
}

Loading…
Cancel
Save