Browse Source

Move Application.Windows to IClassicDesktopStyleApplicationLifetime

pull/2676/head
Nikita Tsukanov 7 years ago
parent
commit
3655b6eea1
  1. 16
      src/Avalonia.Controls/Application.cs
  2. 42
      src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs
  3. 3
      src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs
  4. 41
      src/Avalonia.Controls/Window.cs
  5. 111
      src/Avalonia.Controls/WindowCollection.cs
  6. 88
      tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs
  7. 76
      tests/Avalonia.Controls.UnitTests/WindowTests.cs

16
src/Avalonia.Controls/Application.cs

@ -45,14 +45,6 @@ namespace Avalonia
private Styles _styles; private Styles _styles;
private IResourceDictionary _resources; private IResourceDictionary _resources;
/// <summary>
/// Initializes a new instance of the <see cref="Application"/> class.
/// </summary>
public Application()
{
Windows = new WindowCollection(this);
}
/// <inheritdoc/> /// <inheritdoc/>
public event EventHandler<ResourcesChangedEventArgs> ResourcesChanged; public event EventHandler<ResourcesChangedEventArgs> ResourcesChanged;
@ -159,14 +151,6 @@ namespace Avalonia
/// <inheritdoc/> /// <inheritdoc/>
IResourceNode IResourceNode.ResourceParent => null; IResourceNode IResourceNode.ResourceParent => null;
/// <summary>
/// Gets the open windows of the application.
/// </summary>
/// <value>
/// The windows.
/// </value>
public WindowCollection Windows { get; }
/// <summary> /// <summary>
/// Application lifetime, use it for things like setting the main window and exiting the app from code /// Application lifetime, use it for things like setting the main window and exiting the app from code

42
src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs

@ -1,21 +1,46 @@
using System; using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading; using System.Threading;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Interactivity;
namespace Avalonia.Controls.ApplicationLifetimes namespace Avalonia.Controls.ApplicationLifetimes
{ {
public class ClassicDesktopStyleApplicationLifetime : IClassicDesktopStyleApplicationLifetime public class ClassicDesktopStyleApplicationLifetime : IClassicDesktopStyleApplicationLifetime, IDisposable
{ {
private readonly Application _app; private readonly Application _app;
private int _exitCode; private int _exitCode;
private CancellationTokenSource _cts; private CancellationTokenSource _cts;
private bool _isShuttingDown; 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) 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; _app = app;
app.Windows.OnWindowClosed += HandleWindowClosed; _activeLifetime = this;
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -29,6 +54,8 @@ namespace Avalonia.Controls.ApplicationLifetimes
/// <inheritdoc/> /// <inheritdoc/>
public Window MainWindow { get; set; } public Window MainWindow { get; set; }
public IReadOnlyList<Window> Windows => _windows.ToList();
private void HandleWindowClosed(Window window) private void HandleWindowClosed(Window window)
{ {
if (window == null) if (window == null)
@ -37,7 +64,7 @@ namespace Avalonia.Controls.ApplicationLifetimes
if (_isShuttingDown) if (_isShuttingDown)
return; return;
if (ShutdownMode == ShutdownMode.OnLastWindowClose && _app.Windows.Count == 0) if (ShutdownMode == ShutdownMode.OnLastWindowClose && _windows.Count == 0)
Shutdown(); Shutdown();
else if (ShutdownMode == ShutdownMode.OnMainWindowClose && window == MainWindow) else if (ShutdownMode == ShutdownMode.OnMainWindowClose && window == MainWindow)
Shutdown(); Shutdown();
@ -56,7 +83,8 @@ namespace Avalonia.Controls.ApplicationLifetimes
try try
{ {
_app.Windows.CloseAll(); foreach (var w in Windows)
w.Close();
var e = new ControlledApplicationLifetimeExitEventArgs(exitCode); var e = new ControlledApplicationLifetimeExitEventArgs(exitCode);
Exit?.Invoke(this, e); Exit?.Invoke(this, e);
_exitCode = e.ApplicationExitCode; _exitCode = e.ApplicationExitCode;
@ -79,6 +107,12 @@ namespace Avalonia.Controls.ApplicationLifetimes
Environment.ExitCode = _exitCode; Environment.ExitCode = _exitCode;
return _exitCode; return _exitCode;
} }
public void Dispose()
{
if (_activeLifetime == this)
_activeLifetime = null;
}
} }
} }

3
src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
namespace Avalonia.Controls.ApplicationLifetimes namespace Avalonia.Controls.ApplicationLifetimes
{ {
@ -24,5 +25,7 @@ namespace Avalonia.Controls.ApplicationLifetimes
/// The main window. /// The main window.
/// </value> /// </value>
Window MainWindow { get; set; } Window MainWindow { get; set; }
IReadOnlyList<Window> Windows { get; }
} }
} }

41
src/Avalonia.Controls/Window.cs

@ -14,6 +14,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using JetBrains.Annotations; using JetBrains.Annotations;
using System.ComponentModel; using System.ComponentModel;
using Avalonia.Interactivity;
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
@ -97,6 +98,20 @@ namespace Avalonia.Controls
public static readonly StyledProperty<bool> CanResizeProperty = public static readonly StyledProperty<bool> CanResizeProperty =
AvaloniaProperty.Register<Window, bool>(nameof(CanResize), true); AvaloniaProperty.Register<Window, bool>(nameof(CanResize), true);
/// <summary>
/// Routed event that can be used for global tracking of window destruction
/// </summary>
public static readonly RoutedEvent WindowClosedEvent =
RoutedEvent.Register<Window, RoutedEventArgs>("WindowClosed", RoutingStrategies.Direct);
/// <summary>
/// Routed event that can be used for global tracking of opening windows
/// </summary>
public static readonly RoutedEvent WindowOpenedEvent =
RoutedEvent.Register<Window, RoutedEventArgs>("WindowOpened", RoutingStrategies.Direct);
private readonly NameScope _nameScope = new NameScope(); private readonly NameScope _nameScope = new NameScope();
private object _dialogResult; private object _dialogResult;
private readonly Size _maxPlatformClientSize; private readonly Size _maxPlatformClientSize;
@ -249,26 +264,6 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
public event EventHandler<CancelEventArgs> Closing; 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> /// <summary>
/// Closes the window. /// Closes the window.
/// </summary> /// </summary>
@ -376,7 +371,7 @@ namespace Avalonia.Controls
return; return;
} }
AddWindow(this); this.RaiseEvent(new RoutedEventArgs(WindowOpenedEvent));
EnsureInitialized(); EnsureInitialized();
IsVisible = true; IsVisible = true;
@ -438,7 +433,7 @@ namespace Avalonia.Controls
throw new InvalidOperationException("The window is already being shown."); throw new InvalidOperationException("The window is already being shown.");
} }
AddWindow(this); RaiseEvent(new RoutedEventArgs(WindowOpenedEvent));
EnsureInitialized(); EnsureInitialized();
IsVisible = true; IsVisible = true;
@ -551,7 +546,7 @@ namespace Avalonia.Controls
protected override void HandleClosed() protected override void HandleClosed()
{ {
RemoveWindow(this); RaiseEvent(new RoutedEventArgs(WindowClosedEvent));
base.HandleClosed(); base.HandleClosed();
} }

111
src/Avalonia.Controls/WindowCollection.cs

@ -1,111 +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;
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 event Action<Window> OnWindowClosed;
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>
public void CloseAll()
{
while (_windows.Count > 0)
{
_windows[0].Close(true);
}
}
private void OnRemoveWindow(Window window)
{
if (window != null)
OnWindowClosed?.Invoke(window);
}
}
}

88
tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs

@ -1,8 +1,10 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Platform;
using Avalonia.Threading; using Avalonia.Threading;
using Avalonia.UnitTests; using Avalonia.UnitTests;
using Moq;
using Xunit; using Xunit;
namespace Avalonia.Controls.UnitTests namespace Avalonia.Controls.UnitTests
@ -14,9 +16,8 @@ namespace Avalonia.Controls.UnitTests
public void Should_Set_ExitCode_After_Shutdown() public void Should_Set_ExitCode_After_Shutdown()
{ {
using (UnitTestApplication.Start(TestServices.MockThreadingInterface)) using (UnitTestApplication.Start(TestServices.MockThreadingInterface))
using(var lifetime = new ClassicDesktopStyleApplicationLifetime(Application.Current))
{ {
var lifetime = new ClassicDesktopStyleApplicationLifetime(Application.Current);
Dispatcher.UIThread.InvokeAsync(() => lifetime.Shutdown(1337));
lifetime.Shutdown(1337); lifetime.Shutdown(1337);
var exitCode = lifetime.Start(Array.Empty<string>()); var exitCode = lifetime.Start(Array.Empty<string>());
@ -30,6 +31,7 @@ namespace Avalonia.Controls.UnitTests
public void Should_Close_All_Remaining_Open_Windows_After_Explicit_Exit_Call() public void Should_Close_All_Remaining_Open_Windows_After_Explicit_Exit_Call()
{ {
using (UnitTestApplication.Start(TestServices.StyledWindow)) 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() }; var windows = new List<Window> { new Window(), new Window(), new Window(), new Window() };
@ -37,9 +39,10 @@ namespace Avalonia.Controls.UnitTests
{ {
window.Show(); window.Show();
} }
new ClassicDesktopStyleApplicationLifetime(Application.Current).Shutdown(); Assert.Equal(4, lifetime.Windows.Count);
lifetime.Shutdown();
Assert.Empty(Application.Current.Windows); Assert.Empty(lifetime.Windows);
} }
} }
@ -47,8 +50,8 @@ namespace Avalonia.Controls.UnitTests
public void Should_Only_Exit_On_Explicit_Exit() public void Should_Only_Exit_On_Explicit_Exit()
{ {
using (UnitTestApplication.Start(TestServices.StyledWindow)) using (UnitTestApplication.Start(TestServices.StyledWindow))
using(var lifetime = new ClassicDesktopStyleApplicationLifetime(Application.Current))
{ {
var lifetime = new ClassicDesktopStyleApplicationLifetime(Application.Current);
lifetime.ShutdownMode = ShutdownMode.OnExplicitShutdown; lifetime.ShutdownMode = ShutdownMode.OnExplicitShutdown;
var hasExit = false; var hasExit = false;
@ -81,8 +84,8 @@ namespace Avalonia.Controls.UnitTests
public void Should_Exit_After_MainWindow_Closed() public void Should_Exit_After_MainWindow_Closed()
{ {
using (UnitTestApplication.Start(TestServices.StyledWindow)) using (UnitTestApplication.Start(TestServices.StyledWindow))
using(var lifetime = new ClassicDesktopStyleApplicationLifetime(Application.Current))
{ {
var lifetime = new ClassicDesktopStyleApplicationLifetime(Application.Current);
lifetime.ShutdownMode = ShutdownMode.OnMainWindowClose; lifetime.ShutdownMode = ShutdownMode.OnMainWindowClose;
var hasExit = false; var hasExit = false;
@ -109,8 +112,8 @@ namespace Avalonia.Controls.UnitTests
public void Should_Exit_After_Last_Window_Closed() public void Should_Exit_After_Last_Window_Closed()
{ {
using (UnitTestApplication.Start(TestServices.StyledWindow)) using (UnitTestApplication.Start(TestServices.StyledWindow))
using(var lifetime = new ClassicDesktopStyleApplicationLifetime(Application.Current))
{ {
var lifetime = new ClassicDesktopStyleApplicationLifetime(Application.Current);
lifetime.ShutdownMode = ShutdownMode.OnLastWindowClose; lifetime.ShutdownMode = ShutdownMode.OnLastWindowClose;
var hasExit = false; var hasExit = false;
@ -134,6 +137,77 @@ namespace Avalonia.Controls.UnitTests
Assert.True(hasExit); 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);
}
}
} }
} }

76
tests/Avalonia.Controls.UnitTests/WindowTests.cs

@ -121,75 +121,6 @@ namespace Avalonia.Controls.UnitTests
} }
} }
[Fact]
public void Show_Should_Add_Window_To_OpenWindows()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
ClearOpenWindows();
var window = new Window();
window.Show();
Assert.Equal(new[] { window }, Application.Current.Windows);
}
}
[Fact]
public void Window_Should_Be_Added_To_OpenWindows_Only_Once()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
ClearOpenWindows();
var window = new Window();
window.Show();
window.Show();
window.IsVisible = true;
Assert.Equal(new[] { window }, Application.Current.Windows);
window.Close();
}
}
[Fact]
public void Close_Should_Remove_Window_From_OpenWindows()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
ClearOpenWindows();
var window = new Window();
window.Show();
window.Close();
Assert.Empty(Application.Current.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))
{
ClearOpenWindows();
var window = new Window();
window.Show();
windowImpl.Object.Closed();
Assert.Empty(Application.Current.Windows);
}
}
[Fact] [Fact]
public void Closing_Should_Only_Be_Invoked_Once() public void Closing_Should_Only_Be_Invoked_Once()
{ {
@ -420,12 +351,5 @@ namespace Avalonia.Controls.UnitTests
x.Scaling == 1 && x.Scaling == 1 &&
x.CreateRenderer(It.IsAny<IRenderRoot>()) == renderer.Object); x.CreateRenderer(It.IsAny<IRenderRoot>()) == renderer.Object);
} }
private void ClearOpenWindows()
{
// HACK: We really need a decent way to have "statics" that can be scoped to
// AvaloniaLocator scopes.
Application.Current.Windows.CloseAll();
}
} }
} }

Loading…
Cancel
Save