committed by
GitHub
27 changed files with 615 additions and 767 deletions
@ -0,0 +1,133 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Threading; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Controls.ApplicationLifetimes; |
|||
using Avalonia.Interactivity; |
|||
|
|||
namespace Avalonia.Controls.ApplicationLifetimes |
|||
{ |
|||
public class ClassicDesktopStyleApplicationLifetime : IClassicDesktopStyleApplicationLifetime, IDisposable |
|||
{ |
|||
private readonly Application _app; |
|||
private int _exitCode; |
|||
private CancellationTokenSource _cts; |
|||
private bool _isShuttingDown; |
|||
private HashSet<Window> _windows = new HashSet<Window>(); |
|||
|
|||
private static ClassicDesktopStyleApplicationLifetime _activeLifetime; |
|||
static ClassicDesktopStyleApplicationLifetime() |
|||
{ |
|||
Window.WindowOpenedEvent.AddClassHandler(typeof(Window), OnWindowOpened); |
|||
Window.WindowClosedEvent.AddClassHandler(typeof(Window), WindowClosedEvent); |
|||
} |
|||
|
|||
private static void WindowClosedEvent(object sender, RoutedEventArgs e) |
|||
{ |
|||
_activeLifetime?._windows.Remove((Window)sender); |
|||
_activeLifetime?.HandleWindowClosed((Window)sender); |
|||
} |
|||
|
|||
private static void OnWindowOpened(object sender, RoutedEventArgs e) |
|||
{ |
|||
_activeLifetime?._windows.Add((Window)sender); |
|||
} |
|||
|
|||
public ClassicDesktopStyleApplicationLifetime(Application app) |
|||
{ |
|||
if (_activeLifetime != null) |
|||
throw new InvalidOperationException( |
|||
"Can not have multiple active ClassicDesktopStyleApplicationLifetime instances and the previously created one was not disposed"); |
|||
_app = app; |
|||
_activeLifetime = this; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public event EventHandler<ControlledApplicationLifetimeStartupEventArgs> Startup; |
|||
/// <inheritdoc/>
|
|||
public event EventHandler<ControlledApplicationLifetimeExitEventArgs> Exit; |
|||
|
|||
/// <inheritdoc/>
|
|||
public ShutdownMode ShutdownMode { get; set; } |
|||
|
|||
/// <inheritdoc/>
|
|||
public Window MainWindow { get; set; } |
|||
|
|||
public IReadOnlyList<Window> Windows => _windows.ToList(); |
|||
|
|||
private void HandleWindowClosed(Window window) |
|||
{ |
|||
if (window == null) |
|||
return; |
|||
|
|||
if (_isShuttingDown) |
|||
return; |
|||
|
|||
if (ShutdownMode == ShutdownMode.OnLastWindowClose && _windows.Count == 0) |
|||
Shutdown(); |
|||
else if (ShutdownMode == ShutdownMode.OnMainWindowClose && window == MainWindow) |
|||
Shutdown(); |
|||
} |
|||
|
|||
|
|||
|
|||
|
|||
public void Shutdown(int exitCode = 0) |
|||
{ |
|||
if (_isShuttingDown) |
|||
throw new InvalidOperationException("Application is already shutting down."); |
|||
|
|||
_exitCode = exitCode; |
|||
_isShuttingDown = true; |
|||
|
|||
try |
|||
{ |
|||
foreach (var w in Windows) |
|||
w.Close(); |
|||
var e = new ControlledApplicationLifetimeExitEventArgs(exitCode); |
|||
Exit?.Invoke(this, e); |
|||
_exitCode = e.ApplicationExitCode; |
|||
} |
|||
finally |
|||
{ |
|||
_cts?.Cancel(); |
|||
_cts = null; |
|||
_isShuttingDown = false; |
|||
} |
|||
} |
|||
|
|||
|
|||
public int Start(string[] args) |
|||
{ |
|||
Startup?.Invoke(this, new ControlledApplicationLifetimeStartupEventArgs(args)); |
|||
_cts = new CancellationTokenSource(); |
|||
MainWindow?.Show(); |
|||
_app.Run(_cts.Token); |
|||
Environment.ExitCode = _exitCode; |
|||
return _exitCode; |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
if (_activeLifetime == this) |
|||
_activeLifetime = null; |
|||
} |
|||
} |
|||
} |
|||
|
|||
namespace Avalonia |
|||
{ |
|||
public static class ClassicDesktopStyleApplicationLifetimeExtensions |
|||
{ |
|||
public static int StartWithClassicDesktopLifetime<T>( |
|||
this T builder, string[] args, ShutdownMode shutdownMode = ShutdownMode.OnLastWindowClose) |
|||
where T : AppBuilderBase<T>, new() |
|||
{ |
|||
var lifetime = new ClassicDesktopStyleApplicationLifetime(builder.Instance) {ShutdownMode = shutdownMode}; |
|||
builder.Instance.ApplicationLifetime = lifetime; |
|||
builder.SetupWithoutStarting(); |
|||
return lifetime.Start(args); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,7 @@ |
|||
namespace Avalonia.Controls.ApplicationLifetimes |
|||
{ |
|||
public interface IApplicationLifetime |
|||
{ |
|||
|
|||
} |
|||
} |
|||
@ -0,0 +1,31 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
|
|||
namespace Avalonia.Controls.ApplicationLifetimes |
|||
{ |
|||
/// <summary>
|
|||
/// Controls application lifetime in classic desktop style
|
|||
/// </summary>
|
|||
public interface IClassicDesktopStyleApplicationLifetime : IControlledApplicationLifetime |
|||
{ |
|||
/// <summary>
|
|||
/// 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>
|
|||
ShutdownMode ShutdownMode { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the main window of the application.
|
|||
/// </summary>
|
|||
/// <value>
|
|||
/// The main window.
|
|||
/// </value>
|
|||
Window MainWindow { get; set; } |
|||
|
|||
IReadOnlyList<Window> Windows { get; } |
|||
} |
|||
} |
|||
@ -1,22 +1,19 @@ |
|||
using System; |
|||
|
|||
namespace Avalonia.Controls |
|||
namespace Avalonia.Controls.ApplicationLifetimes |
|||
{ |
|||
/// <summary>
|
|||
/// Sends events about the application lifecycle.
|
|||
/// </summary>
|
|||
public interface IApplicationLifecycle |
|||
public interface IControlledApplicationLifetime : IApplicationLifetime |
|||
{ |
|||
/// <summary>
|
|||
/// Sent when the application is starting up.
|
|||
/// </summary>
|
|||
event EventHandler<StartupEventArgs> Startup; |
|||
event EventHandler<ControlledApplicationLifetimeStartupEventArgs> Startup; |
|||
|
|||
/// <summary>
|
|||
/// Sent when the application is exiting.
|
|||
/// </summary>
|
|||
event EventHandler<ExitEventArgs> Exit; |
|||
|
|||
event EventHandler<ControlledApplicationLifetimeExitEventArgs> Exit; |
|||
|
|||
/// <summary>
|
|||
/// Shuts down the application and sets the exit code that is returned to the operating system when the application exits.
|
|||
/// </summary>
|
|||
@ -0,0 +1,7 @@ |
|||
namespace Avalonia.Controls.ApplicationLifetimes |
|||
{ |
|||
public interface ISingleViewApplicationLifetime : IApplicationLifetime |
|||
{ |
|||
Control MainView { get; set; } |
|||
} |
|||
} |
|||
@ -0,0 +1,22 @@ |
|||
// 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.ApplicationLifetimes |
|||
{ |
|||
/// <summary>
|
|||
/// Contains the arguments for the <see cref="IClassicDesktopStyleApplicationLifetime.Startup"/> event.
|
|||
/// </summary>
|
|||
public class ControlledApplicationLifetimeStartupEventArgs : EventArgs |
|||
{ |
|||
public ControlledApplicationLifetimeStartupEventArgs(IEnumerable<string> args) |
|||
{ |
|||
Args = args?.ToArray() ?? Array.Empty<string>(); |
|||
} |
|||
|
|||
public string[] Args { get; } |
|||
} |
|||
} |
|||
@ -0,0 +1,67 @@ |
|||
using System; |
|||
using System.Threading; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Input; |
|||
using Avalonia.Threading; |
|||
|
|||
namespace Avalonia.Controls |
|||
{ |
|||
public static class DesktopApplicationExtensions |
|||
{ |
|||
[Obsolete("Running application without a cancellation token and a lifetime is no longer supported, see https://github.com/AvaloniaUI/Avalonia/wiki/Application-lifetimes for details")] |
|||
public static void Run(this Application app) => throw new NotSupportedException(); |
|||
|
|||
/// <summary>
|
|||
/// On desktop-style platforms runs the application's main loop until closable is closed
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// Consider using StartWithDesktopStyleLifetime instead, see https://github.com/AvaloniaUI/Avalonia/wiki/Application-lifetimes for details
|
|||
/// </remarks>
|
|||
public static void Run(this Application app, ICloseable closable) |
|||
{ |
|||
var cts = new CancellationTokenSource(); |
|||
closable.Closed += (s, e) => cts.Cancel(); |
|||
|
|||
app.Run(cts.Token); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// On desktop-style platforms runs the application's main loop until main window is closed
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// Consider using StartWithDesktopStyleLifetime instead, see https://github.com/AvaloniaUI/Avalonia/wiki/Application-lifetimes for details
|
|||
/// </remarks>
|
|||
public static void Run(this Application app, Window mainWindow) |
|||
{ |
|||
if (mainWindow == null) |
|||
{ |
|||
throw new ArgumentNullException(nameof(mainWindow)); |
|||
} |
|||
var cts = new CancellationTokenSource(); |
|||
mainWindow.Closed += (_, __) => cts.Cancel(); |
|||
if (!mainWindow.IsVisible) |
|||
{ |
|||
mainWindow.Show(); |
|||
} |
|||
app.Run(cts.Token); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// On desktop-style platforms runs the application's main loop with custom CancellationToken
|
|||
/// without setting a lifetime.
|
|||
/// </summary>
|
|||
/// <param name="token">The token to track.</param>
|
|||
public static void Run(this Application app, CancellationToken token) |
|||
{ |
|||
Dispatcher.UIThread.MainLoop(token); |
|||
} |
|||
|
|||
public static void RunWithMainWindow<TWindow>(this Application app) |
|||
where TWindow : Avalonia.Controls.Window, new() |
|||
{ |
|||
var window = new TWindow(); |
|||
window.Show(); |
|||
app.Run(window); |
|||
} |
|||
} |
|||
} |
|||
@ -1,36 +0,0 @@ |
|||
// 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]; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,134 +0,0 @@ |
|||
// 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(true); |
|||
} |
|||
} |
|||
|
|||
private void OnRemoveWindow(Window window) |
|||
{ |
|||
if (window == null) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
if (_application.IsShuttingDown) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
switch (_application.ShutdownMode) |
|||
{ |
|||
case ShutdownMode.OnLastWindowClose: |
|||
if (Count == 0) |
|||
{ |
|||
_application.Shutdown(); |
|||
} |
|||
|
|||
break; |
|||
case ShutdownMode.OnMainWindowClose: |
|||
if (window == _application.MainWindow) |
|||
{ |
|||
_application.Shutdown(); |
|||
} |
|||
|
|||
break; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,213 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using Avalonia.Controls.ApplicationLifetimes; |
|||
using Avalonia.Platform; |
|||
using Avalonia.Threading; |
|||
using Avalonia.UnitTests; |
|||
using Moq; |
|||
using Xunit; |
|||
|
|||
namespace Avalonia.Controls.UnitTests |
|||
{ |
|||
|
|||
public class DesktopStyleApplicationLifetimeTests |
|||
{ |
|||
[Fact] |
|||
public void Should_Set_ExitCode_After_Shutdown() |
|||
{ |
|||
using (UnitTestApplication.Start(TestServices.MockThreadingInterface)) |
|||
using(var lifetime = new ClassicDesktopStyleApplicationLifetime(Application.Current)) |
|||
{ |
|||
lifetime.Shutdown(1337); |
|||
|
|||
var exitCode = lifetime.Start(Array.Empty<string>()); |
|||
|
|||
Assert.Equal(1337, exitCode); |
|||
} |
|||
} |
|||
|
|||
|
|||
[Fact] |
|||
public void Should_Close_All_Remaining_Open_Windows_After_Explicit_Exit_Call() |
|||
{ |
|||
using (UnitTestApplication.Start(TestServices.StyledWindow)) |
|||
using(var lifetime = new ClassicDesktopStyleApplicationLifetime(Application.Current)) |
|||
{ |
|||
var windows = new List<Window> { new Window(), new Window(), new Window(), new Window() }; |
|||
|
|||
foreach (var window in windows) |
|||
{ |
|||
window.Show(); |
|||
} |
|||
Assert.Equal(4, lifetime.Windows.Count); |
|||
lifetime.Shutdown(); |
|||
|
|||
Assert.Empty(lifetime.Windows); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Only_Exit_On_Explicit_Exit() |
|||
{ |
|||
using (UnitTestApplication.Start(TestServices.StyledWindow)) |
|||
using(var lifetime = new ClassicDesktopStyleApplicationLifetime(Application.Current)) |
|||
{ |
|||
lifetime.ShutdownMode = ShutdownMode.OnExplicitShutdown; |
|||
|
|||
var hasExit = false; |
|||
|
|||
lifetime.Exit += (s, e) => hasExit = true; |
|||
|
|||
var windowA = new Window(); |
|||
|
|||
windowA.Show(); |
|||
|
|||
var windowB = new Window(); |
|||
|
|||
windowB.Show(); |
|||
|
|||
windowA.Close(); |
|||
|
|||
Assert.False(hasExit); |
|||
|
|||
windowB.Close(); |
|||
|
|||
Assert.False(hasExit); |
|||
|
|||
lifetime.Shutdown(); |
|||
|
|||
Assert.True(hasExit); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Exit_After_MainWindow_Closed() |
|||
{ |
|||
using (UnitTestApplication.Start(TestServices.StyledWindow)) |
|||
using(var lifetime = new ClassicDesktopStyleApplicationLifetime(Application.Current)) |
|||
{ |
|||
lifetime.ShutdownMode = ShutdownMode.OnMainWindowClose; |
|||
|
|||
var hasExit = false; |
|||
|
|||
lifetime.Exit += (s, e) => hasExit = true; |
|||
|
|||
var mainWindow = new Window(); |
|||
|
|||
mainWindow.Show(); |
|||
|
|||
lifetime.MainWindow = mainWindow; |
|||
|
|||
var window = new Window(); |
|||
|
|||
window.Show(); |
|||
|
|||
mainWindow.Close(); |
|||
|
|||
Assert.True(hasExit); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Exit_After_Last_Window_Closed() |
|||
{ |
|||
using (UnitTestApplication.Start(TestServices.StyledWindow)) |
|||
using(var lifetime = new ClassicDesktopStyleApplicationLifetime(Application.Current)) |
|||
{ |
|||
lifetime.ShutdownMode = ShutdownMode.OnLastWindowClose; |
|||
|
|||
var hasExit = false; |
|||
|
|||
lifetime.Exit += (s, e) => hasExit = true; |
|||
|
|||
var windowA = new Window(); |
|||
|
|||
windowA.Show(); |
|||
|
|||
var windowB = new Window(); |
|||
|
|||
windowB.Show(); |
|||
|
|||
windowA.Close(); |
|||
|
|||
Assert.False(hasExit); |
|||
|
|||
windowB.Close(); |
|||
|
|||
Assert.True(hasExit); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void Show_Should_Add_Window_To_OpenWindows() |
|||
{ |
|||
using (UnitTestApplication.Start(TestServices.StyledWindow)) |
|||
using(var lifetime = new ClassicDesktopStyleApplicationLifetime(Application.Current)) |
|||
{ |
|||
var window = new Window(); |
|||
|
|||
window.Show(); |
|||
|
|||
Assert.Equal(new[] { window }, lifetime.Windows); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void Window_Should_Be_Added_To_OpenWindows_Only_Once() |
|||
{ |
|||
using (UnitTestApplication.Start(TestServices.StyledWindow)) |
|||
using(var lifetime = new ClassicDesktopStyleApplicationLifetime(Application.Current)) |
|||
{ |
|||
var window = new Window(); |
|||
|
|||
window.Show(); |
|||
window.Show(); |
|||
window.IsVisible = true; |
|||
|
|||
Assert.Equal(new[] { window }, lifetime.Windows); |
|||
|
|||
window.Close(); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void Close_Should_Remove_Window_From_OpenWindows() |
|||
{ |
|||
using (UnitTestApplication.Start(TestServices.StyledWindow)) |
|||
using(var lifetime = new ClassicDesktopStyleApplicationLifetime(Application.Current)) |
|||
{ |
|||
var window = new Window(); |
|||
|
|||
window.Show(); |
|||
Assert.Equal(1, lifetime.Windows.Count); |
|||
window.Close(); |
|||
|
|||
Assert.Empty(lifetime.Windows); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void Impl_Closing_Should_Remove_Window_From_OpenWindows() |
|||
{ |
|||
var windowImpl = new Mock<IWindowImpl>(); |
|||
windowImpl.SetupProperty(x => x.Closed); |
|||
windowImpl.Setup(x => x.Scaling).Returns(1); |
|||
|
|||
var services = TestServices.StyledWindow.With( |
|||
windowingPlatform: new MockWindowingPlatform(() => windowImpl.Object)); |
|||
|
|||
using (UnitTestApplication.Start(services)) |
|||
using(var lifetime = new ClassicDesktopStyleApplicationLifetime(Application.Current)) |
|||
{ |
|||
var window = new Window(); |
|||
|
|||
window.Show(); |
|||
Assert.Equal(1, lifetime.Windows.Count); |
|||
windowImpl.Object.Closed(); |
|||
|
|||
Assert.Empty(lifetime.Windows); |
|||
} |
|||
} |
|||
} |
|||
|
|||
} |
|||
Loading…
Reference in new issue