Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Application ExitMode #1617

Closed
wants to merge 11 commits into from
45 changes: 27 additions & 18 deletions src/Avalonia.Controls/AppBuilderBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ namespace Avalonia.Controls
public abstract class AppBuilderBase<TAppBuilder> where TAppBuilder : AppBuilderBase<TAppBuilder>, new()
{
private static bool s_setupWasAlreadyCalled;

/// <summary>
/// Gets or sets the <see cref="IRuntimePlatform"/> instance.
/// </summary>
Expand Down Expand Up @@ -92,7 +92,7 @@ public static TAppBuilder Configure(Application app)
};
}

protected TAppBuilder Self => (TAppBuilder) this;
protected TAppBuilder Self => (TAppBuilder)this;

/// <summary>
/// Registers a callback to call before Start is called on the <see cref="Application"/>.
Expand Down Expand Up @@ -125,7 +125,6 @@ public void Start<TMainWindow>(Func<object> dataContextProvider = null)
var window = new TMainWindow();
if (dataContextProvider != null)
window.DataContext = dataContextProvider();
window.Show();
Instance.Run(window);
}

Expand All @@ -143,7 +142,6 @@ public void Start<TMainWindow>(TMainWindow mainWindow, Func<object> dataContextP

if (dataContextProvider != null)
mainWindow.DataContext = dataContextProvider();
mainWindow.Show();
Instance.Run(mainWindow);
}

Expand Down Expand Up @@ -209,6 +207,17 @@ static Action GetInitializer(string assemblyName) => () =>

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;

/// <summary>
Expand All @@ -223,20 +232,20 @@ internal TAppBuilder IgnoreSetupCheck()
private void SetupAvaloniaModules()
{
var moduleInitializers = from assembly in AvaloniaLocator.Current.GetService<IRuntimePlatform>().GetLoadedAssemblies()
from attribute in assembly.GetCustomAttributes<ExportAvaloniaModuleAttribute>()
where attribute.ForWindowingSubsystem == ""
|| attribute.ForWindowingSubsystem == WindowingSubsystemName
where attribute.ForRenderingSubsystem == ""
|| attribute.ForRenderingSubsystem == RenderingSubsystemName
group attribute by attribute.Name into exports
select (from export in exports
orderby export.ForWindowingSubsystem.Length descending
orderby export.ForRenderingSubsystem.Length descending
select export).First().ModuleType into moduleType
select (from constructor in moduleType.GetTypeInfo().DeclaredConstructors
where constructor.GetParameters().Length == 0 && !constructor.IsStatic
select constructor).Single() into constructor
select (Action)(() => constructor.Invoke(new object[0]));
from attribute in assembly.GetCustomAttributes<ExportAvaloniaModuleAttribute>()
where attribute.ForWindowingSubsystem == ""
|| attribute.ForWindowingSubsystem == WindowingSubsystemName
where attribute.ForRenderingSubsystem == ""
|| attribute.ForRenderingSubsystem == RenderingSubsystemName
group attribute by attribute.Name into exports
select (from export in exports
orderby export.ForWindowingSubsystem.Length descending
orderby export.ForRenderingSubsystem.Length descending
select export).First().ModuleType into moduleType
select (from constructor in moduleType.GetTypeInfo().DeclaredConstructors
where constructor.GetParameters().Length == 0 && !constructor.IsStatic
select constructor).Single() into constructor
select (Action)(() => constructor.Invoke(new object[0]));
Delegate.Combine(moduleInitializers.ToArray()).DynamicInvoke();
}

Expand Down
118 changes: 112 additions & 6 deletions src/Avalonia.Controls/Application.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,15 @@ public class Application : IApplicationLifecycle, IGlobalDataTemplates, IGlobalS
private Styles _styles;
private IResourceDictionary _resources;

private CancellationTokenSource _mainLoopCancellationTokenSource;

/// <summary>
/// Initializes a new instance of the <see cref="Application"/> class.
/// </summary>
public Application()
{
Windows = new WindowCollection(this);

OnExit += OnExiting;
}

Expand Down Expand Up @@ -158,6 +162,40 @@ public IResourceDictionary Resources
/// <inheritdoc/>
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>
/// Initializes the application by loading XAML etc.
/// </summary>
Expand All @@ -171,27 +209,95 @@ public virtual void Initialize()
/// <param name="closable">The closable to track</param>
public void Run(ICloseable closable)
{
var source = new CancellationTokenSource();
closable.Closed += OnExiting;
closable.Closed += (s, e) => source.Cancel();
Dispatcher.UIThread.MainLoop(source.Token);
if (_mainLoopCancellationTokenSource != null)
{
throw new Exception("Run should only called once");
}

closable.Closed += (s, e) => Exit();

_mainLoopCancellationTokenSource = new CancellationTokenSource();

Dispatcher.UIThread.MainLoop(_mainLoopCancellationTokenSource.Token);

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

if (!IsExiting)
{
OnExit?.Invoke(this, EventArgs.Empty);
}
}

/// <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>
/// <param name="token">The token to track</param>
public void Run(CancellationToken token)
{
Dispatcher.UIThread.MainLoop(token);

if (!IsExiting)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is not exiting?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This case is needed to call Exit if an error happened. We know that an error happened if Exit wasn't called explicitly so IsExiting isn't set.

{
OnExit?.Invoke(this, EventArgs.Empty);
}
}

/// <summary>
/// Exits the application
/// </summary>
public void Exit()
{
IsExiting = true;

while (Windows.Count > 0)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I have tow Windows that this loop never exit.

I think you should write remove the first windows.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's wrong because this line always removes the first window of the collection until none is left. A close removes the window from the list. I will add unit tests to prove that.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Gillibald Good

{
Windows[0].Close();
}

OnExit?.Invoke(this, EventArgs.Empty);

_mainLoopCancellationTokenSource?.Cancel();
}

/// <inheritdoc/>
Expand Down
12 changes: 12 additions & 0 deletions src/Avalonia.Controls/ExitMode.cs
Original file line number Diff line number Diff line change
@@ -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 changes: 31 additions & 20 deletions src/Avalonia.Controls/Window.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,6 @@ public enum SizeToContent
/// </summary>
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.
/// </summary>
public static readonly StyledProperty<SizeToContent> SizeToContentProperty =
Expand All @@ -75,7 +67,7 @@ public class Window : WindowBase, IStyleable, IFocusScope, ILayoutRoot, INameSco
AvaloniaProperty.Register<Window, bool>(nameof(ShowInTaskbar), true);

/// <summary>
/// Enables or disables the taskbar icon
/// Represents the current window state (normal, minimized, maximized)
/// </summary>
public static readonly StyledProperty<WindowState> WindowStateProperty =
AvaloniaProperty.Register<Window, WindowState>(nameof(WindowState));
Expand Down Expand Up @@ -117,7 +109,7 @@ static Window()
BackgroundProperty.OverrideDefaultValue(typeof(Window), Brushes.White);
TitleProperty.Changed.AddClassHandler<Window>((s, e) => s.PlatformImpl?.SetTitle((string)e.NewValue));
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));

Expand Down Expand Up @@ -149,7 +141,7 @@ public Window(IWindowImpl impl)
_maxPlatformClientSize = PlatformImpl?.MaxClientSize ?? default(Size);
Screens = new Screens(PlatformImpl?.Screen);
}

/// <inheritdoc/>
event EventHandler<NameScopeEventArgs> INameScope.Registered
{
Expand Down Expand Up @@ -199,7 +191,7 @@ public bool HasSystemDecorations
get { return GetValue(HasSystemDecorationsProperty); }
set { SetValue(HasSystemDecorationsProperty, value); }
}

/// <summary>
/// Enables or disables the taskbar icon
/// </summary>
Expand Down Expand Up @@ -259,6 +251,26 @@ public WindowStartupLocation WindowStartupLocation
/// </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>
Expand Down Expand Up @@ -298,10 +310,9 @@ internal void Close(bool ignoreCancel)
finally
{
if (ignoreCancel || !cancelClosing)
{
s_windows.Remove(this);
{
PlatformImpl?.Dispose();
IsVisible = false;
HandleClosed();
}
}
}
Expand Down Expand Up @@ -359,7 +370,7 @@ public override void Show()
return;
}

s_windows.Add(this);
AddWindow(this);

EnsureInitialized();
SetWindowStartupLocation();
Expand Down Expand Up @@ -400,7 +411,7 @@ public Task<TResult> ShowDialog<TResult>()
throw new InvalidOperationException("The window is already being shown.");
}

s_windows.Add(this);
AddWindow(this);

EnsureInitialized();
SetWindowStartupLocation();
Expand All @@ -409,7 +420,7 @@ public Task<TResult> ShowDialog<TResult>()

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();
SetIsEnabled(affectedWindows, false);

Expand Down Expand Up @@ -513,8 +524,8 @@ protected override Size MeasureOverride(Size availableSize)

protected override void HandleClosed()
{
IsVisible = false;
s_windows.Remove(this);
RemoveWindow(this);

base.HandleClosed();
}

Expand Down
Loading