Browse Source

Initial

pull/1662/head
Benedikt Schroeder 8 years ago
parent
commit
e576ec178c
  1. 45
      src/Avalonia.Controls/AppBuilderBase.cs
  2. 118
      src/Avalonia.Controls/Application.cs
  3. 12
      src/Avalonia.Controls/ExitMode.cs
  4. 51
      src/Avalonia.Controls/Window.cs
  5. 134
      src/Avalonia.Controls/WindowCollection.cs
  6. 107
      tests/Avalonia.Controls.UnitTests/ApplicationTests.cs
  7. 10
      tests/Avalonia.Controls.UnitTests/WindowTests.cs

45
src/Avalonia.Controls/AppBuilderBase.cs

@ -15,7 +15,7 @@ namespace Avalonia.Controls
public abstract class AppBuilderBase<TAppBuilder> where TAppBuilder : AppBuilderBase<TAppBuilder>, new() public abstract class AppBuilderBase<TAppBuilder> where TAppBuilder : AppBuilderBase<TAppBuilder>, new()
{ {
private static bool s_setupWasAlreadyCalled; private static bool s_setupWasAlreadyCalled;
/// <summary> /// <summary>
/// Gets or sets the <see cref="IRuntimePlatform"/> instance. /// Gets or sets the <see cref="IRuntimePlatform"/> instance.
/// </summary> /// </summary>
@ -92,7 +92,7 @@ namespace Avalonia.Controls
}; };
} }
protected TAppBuilder Self => (TAppBuilder) this; protected TAppBuilder Self => (TAppBuilder)this;
/// <summary> /// <summary>
/// Registers a callback to call before Start is called on the <see cref="Application"/>. /// Registers a callback to call before Start is called on the <see cref="Application"/>.
@ -125,7 +125,6 @@ namespace Avalonia.Controls
var window = new TMainWindow(); var window = new TMainWindow();
if (dataContextProvider != null) if (dataContextProvider != null)
window.DataContext = dataContextProvider(); window.DataContext = dataContextProvider();
window.Show();
Instance.Run(window); Instance.Run(window);
} }
@ -143,7 +142,6 @@ namespace Avalonia.Controls
if (dataContextProvider != null) if (dataContextProvider != null)
mainWindow.DataContext = dataContextProvider(); mainWindow.DataContext = dataContextProvider();
mainWindow.Show();
Instance.Run(mainWindow); Instance.Run(mainWindow);
} }
@ -209,6 +207,17 @@ namespace Avalonia.Controls
public TAppBuilder UseAvaloniaModules() => AfterSetup(builder => SetupAvaloniaModules()); public TAppBuilder UseAvaloniaModules() => AfterSetup(builder => SetupAvaloniaModules());
/// <summary>
/// Sets the shutdown mode of the application.
/// </summary>
/// <param name="exitMode">The shutdown mode.</param>
/// <returns></returns>
public TAppBuilder SetExitMode(ExitMode exitMode)
{
Instance.ExitMode = exitMode;
return Self;
}
private bool CheckSetup { get; set; } = true; private bool CheckSetup { get; set; } = true;
/// <summary> /// <summary>
@ -223,20 +232,20 @@ namespace Avalonia.Controls
private void SetupAvaloniaModules() private void SetupAvaloniaModules()
{ {
var moduleInitializers = from assembly in AvaloniaLocator.Current.GetService<IRuntimePlatform>().GetLoadedAssemblies() var moduleInitializers = from assembly in AvaloniaLocator.Current.GetService<IRuntimePlatform>().GetLoadedAssemblies()
from attribute in assembly.GetCustomAttributes<ExportAvaloniaModuleAttribute>() from attribute in assembly.GetCustomAttributes<ExportAvaloniaModuleAttribute>()
where attribute.ForWindowingSubsystem == "" where attribute.ForWindowingSubsystem == ""
|| attribute.ForWindowingSubsystem == WindowingSubsystemName || attribute.ForWindowingSubsystem == WindowingSubsystemName
where attribute.ForRenderingSubsystem == "" where attribute.ForRenderingSubsystem == ""
|| attribute.ForRenderingSubsystem == RenderingSubsystemName || attribute.ForRenderingSubsystem == RenderingSubsystemName
group attribute by attribute.Name into exports group attribute by attribute.Name into exports
select (from export in exports select (from export in exports
orderby export.ForWindowingSubsystem.Length descending orderby export.ForWindowingSubsystem.Length descending
orderby export.ForRenderingSubsystem.Length descending orderby export.ForRenderingSubsystem.Length descending
select export).First().ModuleType into moduleType select export).First().ModuleType into moduleType
select (from constructor in moduleType.GetTypeInfo().DeclaredConstructors select (from constructor in moduleType.GetTypeInfo().DeclaredConstructors
where constructor.GetParameters().Length == 0 && !constructor.IsStatic where constructor.GetParameters().Length == 0 && !constructor.IsStatic
select constructor).Single() into constructor select constructor).Single() into constructor
select (Action)(() => constructor.Invoke(new object[0])); select (Action)(() => constructor.Invoke(new object[0]));
Delegate.Combine(moduleInitializers.ToArray()).DynamicInvoke(); Delegate.Combine(moduleInitializers.ToArray()).DynamicInvoke();
} }

118
src/Avalonia.Controls/Application.cs

@ -43,11 +43,15 @@ namespace Avalonia
private Styles _styles; private Styles _styles;
private IResourceDictionary _resources; private IResourceDictionary _resources;
private CancellationTokenSource _mainLoopCancellationTokenSource;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="Application"/> class. /// Initializes a new instance of the <see cref="Application"/> class.
/// </summary> /// </summary>
public Application() public Application()
{ {
Windows = new WindowCollection(this);
OnExit += OnExiting; OnExit += OnExiting;
} }
@ -158,6 +162,40 @@ namespace Avalonia
/// <inheritdoc/> /// <inheritdoc/>
IResourceNode IResourceNode.ResourceParent => null; 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.
/// The default is OnLastWindowClose
/// </summary>
/// <value>
/// The shutdown mode.
/// </value>
public ExitMode ExitMode { get; set; }
/// <summary>
/// Gets or sets the main window of the application.
/// </summary>
/// <value>
/// The main window.
/// </value>
public Window MainWindow { get; set; }
/// <summary>
/// Gets the open windows of the application.
/// </summary>
/// <value>
/// The windows.
/// </value>
public WindowCollection Windows { get; }
/// <summary>
/// Gets or sets a value indicating whether this instance is existing.
/// </summary>
/// <value>
/// <c>true</c> if this instance is existing; otherwise, <c>false</c>.
/// </value>
internal bool IsExiting { get; set; }
/// <summary> /// <summary>
/// Initializes the application by loading XAML etc. /// Initializes the application by loading XAML etc.
/// </summary> /// </summary>
@ -171,19 +209,81 @@ namespace Avalonia
/// <param name="closable">The closable to track</param> /// <param name="closable">The closable to track</param>
public void Run(ICloseable closable) public void Run(ICloseable closable)
{ {
var source = new CancellationTokenSource(); if (_mainLoopCancellationTokenSource != null)
closable.Closed += OnExiting; {
closable.Closed += (s, e) => source.Cancel(); throw new Exception("Run should only called once");
Dispatcher.UIThread.MainLoop(source.Token); }
closable.Closed += (s, e) => Exit();
_mainLoopCancellationTokenSource = new CancellationTokenSource();
Dispatcher.UIThread.MainLoop(_mainLoopCancellationTokenSource.Token);
// Make sure we call OnExit in case an error happened and Exit() wasn't called explicitly
if (!IsExiting)
{
OnExit?.Invoke(this, EventArgs.Empty);
}
}
/// <summary>
/// Runs the application's main loop until some condition occurs that is specified by ExitMode.
/// </summary>
/// <param name="mainWindow">The main window</param>
public void Run(Window mainWindow)
{
if (_mainLoopCancellationTokenSource != null)
{
throw new Exception("Run should only called once");
}
_mainLoopCancellationTokenSource = new CancellationTokenSource();
Dispatcher.UIThread.InvokeAsync(
() =>
{
if (mainWindow == null)
{
return;
}
if (MainWindow != null)
{
return;
}
if (!mainWindow.IsVisible)
{
mainWindow.Show();
}
MainWindow = mainWindow;
},
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)
{
OnExit?.Invoke(this, EventArgs.Empty);
}
} }
/// <summary> /// <summary>
/// Runs the application's main loop until the <see cref="CancellationToken"/> is cancelled. /// Runs the application's main loop until the <see cref="CancellationToken"/> is canceled.
/// </summary> /// </summary>
/// <param name="token">The token to track</param> /// <param name="token">The token to track</param>
public void Run(CancellationToken token) public void Run(CancellationToken token)
{ {
Dispatcher.UIThread.MainLoop(token); 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);
}
} }
/// <summary> /// <summary>
@ -191,7 +291,13 @@ namespace Avalonia
/// </summary> /// </summary>
public void Exit() public void Exit()
{ {
IsExiting = true;
Windows.Clear();
OnExit?.Invoke(this, EventArgs.Empty); OnExit?.Invoke(this, EventArgs.Empty);
_mainLoopCancellationTokenSource?.Cancel();
} }
/// <inheritdoc/> /// <inheritdoc/>

12
src/Avalonia.Controls/ExitMode.cs

@ -0,0 +1,12 @@
// 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
{
public enum ExitMode
{
OnLastWindowClose,
OnMainWindowClose,
OnExplicitExit
}
}

51
src/Avalonia.Controls/Window.cs

@ -49,14 +49,6 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
public class Window : WindowBase, IStyleable, IFocusScope, ILayoutRoot, INameScope public class Window : WindowBase, IStyleable, IFocusScope, ILayoutRoot, INameScope
{ {
private static List<Window> s_windows = new List<Window>();
/// <summary>
/// Retrieves an enumeration of all Windows in the currently running application.
/// </summary>
public static IReadOnlyList<Window> OpenWindows => s_windows;
/// <summary>
/// Defines the <see cref="SizeToContent"/> property. /// Defines the <see cref="SizeToContent"/> property.
/// </summary> /// </summary>
public static readonly StyledProperty<SizeToContent> SizeToContentProperty = public static readonly StyledProperty<SizeToContent> SizeToContentProperty =
@ -75,7 +67,7 @@ namespace Avalonia.Controls
AvaloniaProperty.Register<Window, bool>(nameof(ShowInTaskbar), true); AvaloniaProperty.Register<Window, bool>(nameof(ShowInTaskbar), true);
/// <summary> /// <summary>
/// Enables or disables the taskbar icon /// Represents the current window state (normal, minimized, maximized)
/// </summary> /// </summary>
public static readonly StyledProperty<WindowState> WindowStateProperty = public static readonly StyledProperty<WindowState> WindowStateProperty =
AvaloniaProperty.Register<Window, WindowState>(nameof(WindowState)); AvaloniaProperty.Register<Window, WindowState>(nameof(WindowState));
@ -117,7 +109,7 @@ namespace Avalonia.Controls
BackgroundProperty.OverrideDefaultValue(typeof(Window), Brushes.White); BackgroundProperty.OverrideDefaultValue(typeof(Window), Brushes.White);
TitleProperty.Changed.AddClassHandler<Window>((s, e) => s.PlatformImpl?.SetTitle((string)e.NewValue)); TitleProperty.Changed.AddClassHandler<Window>((s, e) => s.PlatformImpl?.SetTitle((string)e.NewValue));
HasSystemDecorationsProperty.Changed.AddClassHandler<Window>( HasSystemDecorationsProperty.Changed.AddClassHandler<Window>(
(s, e) => s.PlatformImpl?.SetSystemDecorations((bool) e.NewValue)); (s, e) => s.PlatformImpl?.SetSystemDecorations((bool)e.NewValue));
ShowInTaskbarProperty.Changed.AddClassHandler<Window>((w, e) => w.PlatformImpl?.ShowTaskbarIcon((bool)e.NewValue)); ShowInTaskbarProperty.Changed.AddClassHandler<Window>((w, e) => w.PlatformImpl?.ShowTaskbarIcon((bool)e.NewValue));
@ -149,7 +141,7 @@ namespace Avalonia.Controls
_maxPlatformClientSize = PlatformImpl?.MaxClientSize ?? default(Size); _maxPlatformClientSize = PlatformImpl?.MaxClientSize ?? default(Size);
Screens = new Screens(PlatformImpl?.Screen); Screens = new Screens(PlatformImpl?.Screen);
} }
/// <inheritdoc/> /// <inheritdoc/>
event EventHandler<NameScopeEventArgs> INameScope.Registered event EventHandler<NameScopeEventArgs> INameScope.Registered
{ {
@ -199,7 +191,7 @@ namespace Avalonia.Controls
get { return GetValue(HasSystemDecorationsProperty); } get { return GetValue(HasSystemDecorationsProperty); }
set { SetValue(HasSystemDecorationsProperty, value); } set { SetValue(HasSystemDecorationsProperty, value); }
} }
/// <summary> /// <summary>
/// Enables or disables the taskbar icon /// Enables or disables the taskbar icon
/// </summary> /// </summary>
@ -259,6 +251,26 @@ 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>
@ -298,10 +310,9 @@ namespace Avalonia.Controls
finally finally
{ {
if (ignoreCancel || !cancelClosing) if (ignoreCancel || !cancelClosing)
{ {
s_windows.Remove(this);
PlatformImpl?.Dispose(); PlatformImpl?.Dispose();
IsVisible = false; HandleClosed();
} }
} }
} }
@ -359,7 +370,7 @@ namespace Avalonia.Controls
return; return;
} }
s_windows.Add(this); AddWindow(this);
EnsureInitialized(); EnsureInitialized();
SetWindowStartupLocation(); SetWindowStartupLocation();
@ -400,7 +411,7 @@ namespace Avalonia.Controls
throw new InvalidOperationException("The window is already being shown."); throw new InvalidOperationException("The window is already being shown.");
} }
s_windows.Add(this); AddWindow(this);
EnsureInitialized(); EnsureInitialized();
SetWindowStartupLocation(); SetWindowStartupLocation();
@ -409,7 +420,7 @@ namespace Avalonia.Controls
using (BeginAutoSizing()) using (BeginAutoSizing())
{ {
var affectedWindows = s_windows.Where(w => w.IsEnabled && w != this).ToList(); var affectedWindows = Application.Current.Windows.Where(w => w.IsEnabled && w != this).ToList();
var activated = affectedWindows.Where(w => w.IsActive).FirstOrDefault(); var activated = affectedWindows.Where(w => w.IsActive).FirstOrDefault();
SetIsEnabled(affectedWindows, false); SetIsEnabled(affectedWindows, false);
@ -513,8 +524,8 @@ namespace Avalonia.Controls
protected override void HandleClosed() protected override void HandleClosed()
{ {
IsVisible = false; RemoveWindow(this);
s_windows.Remove(this);
base.HandleClosed(); base.HandleClosed();
} }

134
src/Avalonia.Controls/WindowCollection.cs

@ -0,0 +1,134 @@
// 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();
}
}
private void OnRemoveWindow(Window window)
{
if (window == null)
{
return;
}
if (_application.IsExiting)
{
return;
}
switch (_application.ExitMode)
{
case ExitMode.OnLastWindowClose:
if (Count == 0)
{
_application.Exit();
}
break;
case ExitMode.OnMainWindowClose:
if (window == _application.MainWindow)
{
_application.Exit();
}
break;
}
}
}
}

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

@ -0,0 +1,107 @@
// 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.Generic;
using Avalonia.UnitTests;
using Xunit;
namespace Avalonia.Controls.UnitTests
{
public class ApplicationTests
{
[Fact]
public void Should_Exit_After_MainWindow_Closed()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
Application.Current.ExitMode = ExitMode.OnMainWindowClose;
var mainWindow = new Window();
mainWindow.Show();
Application.Current.MainWindow = mainWindow;
var window = new Window();
window.Show();
mainWindow.Close();
Assert.True(Application.Current.IsExiting);
}
}
[Fact]
public void Should_Exit_After_Last_Window_Closed()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
Application.Current.ExitMode = ExitMode.OnLastWindowClose;
var windowA = new Window();
windowA.Show();
var windowB = new Window();
windowB.Show();
windowA.Close();
Assert.False(Application.Current.IsExiting);
windowB.Close();
Assert.True(Application.Current.IsExiting);
}
}
[Fact]
public void Should_Only_Exit_On_Explicit_Exit()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
Application.Current.ExitMode = ExitMode.OnExplicitExit;
var windowA = new Window();
windowA.Show();
var windowB = new Window();
windowB.Show();
windowA.Close();
Assert.False(Application.Current.IsExiting);
windowB.Close();
Assert.False(Application.Current.IsExiting);
Application.Current.Exit();
Assert.True(Application.Current.IsExiting);
}
}
[Fact]
public void Should_Close_All_Remaining_Open_Windows_After_Explicit_Exit_Call()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var windows = new List<Window> { new Window(), new Window(), new Window(), new Window() };
foreach (var window in windows)
{
window.Show();
}
Application.Current.Exit();
Assert.Empty(Application.Current.Windows);
}
}
}
}

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

@ -129,7 +129,7 @@ namespace Avalonia.Controls.UnitTests
window.Show(); window.Show();
Assert.Equal(new[] { window }, Window.OpenWindows); Assert.Equal(new[] { window }, Application.Current.Windows);
} }
} }
@ -145,7 +145,7 @@ namespace Avalonia.Controls.UnitTests
window.Show(); window.Show();
window.IsVisible = true; window.IsVisible = true;
Assert.Equal(new[] { window }, Window.OpenWindows); Assert.Equal(new[] { window }, Application.Current.Windows);
window.Close(); window.Close();
} }
@ -162,7 +162,7 @@ namespace Avalonia.Controls.UnitTests
window.Show(); window.Show();
window.Close(); window.Close();
Assert.Empty(Window.OpenWindows); Assert.Empty(Application.Current.Windows);
} }
} }
@ -184,7 +184,7 @@ namespace Avalonia.Controls.UnitTests
window.Show(); window.Show();
windowImpl.Object.Closed(); windowImpl.Object.Closed();
Assert.Empty(Window.OpenWindows); Assert.Empty(Application.Current.Windows);
} }
} }
@ -339,7 +339,7 @@ namespace Avalonia.Controls.UnitTests
{ {
// HACK: We really need a decent way to have "statics" that can be scoped to // HACK: We really need a decent way to have "statics" that can be scoped to
// AvaloniaLocator scopes. // AvaloniaLocator scopes.
((IList<Window>)Window.OpenWindows).Clear(); Application.Current.Windows.Clear();
} }
} }
} }

Loading…
Cancel
Save