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 IResourceDictionary _resources;
/// <summary>
/// Initializes a new instance of the <see cref="Application"/> class.
/// </summary>
public Application()
{
Windows = new WindowCollection(this);
}
/// <inheritdoc/>
public event EventHandler<ResourcesChangedEventArgs> ResourcesChanged;
@ -159,14 +151,6 @@ namespace Avalonia
/// <inheritdoc/>
IResourceNode IResourceNode.ResourceParent => null;
/// <summary>
/// Gets the open windows of the application.
/// </summary>
/// <value>
/// The windows.
/// </value>
public WindowCollection Windows { get; }
/// <summary>
/// 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.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
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;
app.Windows.OnWindowClosed += HandleWindowClosed;
_activeLifetime = this;
}
/// <inheritdoc/>
@ -29,6 +54,8 @@ namespace Avalonia.Controls.ApplicationLifetimes
/// <inheritdoc/>
public Window MainWindow { get; set; }
public IReadOnlyList<Window> Windows => _windows.ToList();
private void HandleWindowClosed(Window window)
{
if (window == null)
@ -37,7 +64,7 @@ namespace Avalonia.Controls.ApplicationLifetimes
if (_isShuttingDown)
return;
if (ShutdownMode == ShutdownMode.OnLastWindowClose && _app.Windows.Count == 0)
if (ShutdownMode == ShutdownMode.OnLastWindowClose && _windows.Count == 0)
Shutdown();
else if (ShutdownMode == ShutdownMode.OnMainWindowClose && window == MainWindow)
Shutdown();
@ -56,7 +83,8 @@ namespace Avalonia.Controls.ApplicationLifetimes
try
{
_app.Windows.CloseAll();
foreach (var w in Windows)
w.Close();
var e = new ControlledApplicationLifetimeExitEventArgs(exitCode);
Exit?.Invoke(this, e);
_exitCode = e.ApplicationExitCode;
@ -79,6 +107,12 @@ namespace Avalonia.Controls.ApplicationLifetimes
Environment.ExitCode = _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.Collections.Generic;
namespace Avalonia.Controls.ApplicationLifetimes
{
@ -24,5 +25,7 @@ namespace Avalonia.Controls.ApplicationLifetimes
/// The main window.
/// </value>
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 JetBrains.Annotations;
using System.ComponentModel;
using Avalonia.Interactivity;
namespace Avalonia.Controls
{
@ -97,6 +98,20 @@ namespace Avalonia.Controls
public static readonly StyledProperty<bool> CanResizeProperty =
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 object _dialogResult;
private readonly Size _maxPlatformClientSize;
@ -249,26 +264,6 @@ namespace Avalonia.Controls
/// </summary>
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>
/// Closes the window.
/// </summary>
@ -376,7 +371,7 @@ namespace Avalonia.Controls
return;
}
AddWindow(this);
this.RaiseEvent(new RoutedEventArgs(WindowOpenedEvent));
EnsureInitialized();
IsVisible = true;
@ -438,7 +433,7 @@ namespace Avalonia.Controls
throw new InvalidOperationException("The window is already being shown.");
}
AddWindow(this);
RaiseEvent(new RoutedEventArgs(WindowOpenedEvent));
EnsureInitialized();
IsVisible = true;
@ -551,7 +546,7 @@ namespace Avalonia.Controls
protected override void HandleClosed()
{
RemoveWindow(this);
RaiseEvent(new RoutedEventArgs(WindowClosedEvent));
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.Collections.Generic;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Platform;
using Avalonia.Threading;
using Avalonia.UnitTests;
using Moq;
using Xunit;
namespace Avalonia.Controls.UnitTests
@ -14,9 +16,8 @@ namespace Avalonia.Controls.UnitTests
public void Should_Set_ExitCode_After_Shutdown()
{
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);
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()
{
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() };
@ -37,9 +39,10 @@ namespace Avalonia.Controls.UnitTests
{
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()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
using(var lifetime = new ClassicDesktopStyleApplicationLifetime(Application.Current))
{
var lifetime = new ClassicDesktopStyleApplicationLifetime(Application.Current);
lifetime.ShutdownMode = ShutdownMode.OnExplicitShutdown;
var hasExit = false;
@ -81,8 +84,8 @@ namespace Avalonia.Controls.UnitTests
public void Should_Exit_After_MainWindow_Closed()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
using(var lifetime = new ClassicDesktopStyleApplicationLifetime(Application.Current))
{
var lifetime = new ClassicDesktopStyleApplicationLifetime(Application.Current);
lifetime.ShutdownMode = ShutdownMode.OnMainWindowClose;
var hasExit = false;
@ -109,8 +112,8 @@ namespace Avalonia.Controls.UnitTests
public void Should_Exit_After_Last_Window_Closed()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
using(var lifetime = new ClassicDesktopStyleApplicationLifetime(Application.Current))
{
var lifetime = new ClassicDesktopStyleApplicationLifetime(Application.Current);
lifetime.ShutdownMode = ShutdownMode.OnLastWindowClose;
var hasExit = false;
@ -134,6 +137,77 @@ namespace Avalonia.Controls.UnitTests
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]
public void Closing_Should_Only_Be_Invoked_Once()
{
@ -420,12 +351,5 @@ namespace Avalonia.Controls.UnitTests
x.Scaling == 1 &&
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