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

Fix ClassicDesktop Lifetime so that ShutdownRequested event is raised… #7400

Merged
merged 10 commits into from
Jan 20, 2022
3 changes: 2 additions & 1 deletion src/Avalonia.Controls/ApiCompatBaseline.txt
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ CannotRemoveBaseTypeOrInterface : Type 'Avalonia.Controls.WindowBase' does not i
InterfacesShouldHaveSameMembers : Interface member 'public System.EventHandler<Avalonia.Controls.ApplicationLifetimes.ShutdownRequestedEventArgs> Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime.ShutdownRequested' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime.add_ShutdownRequested(System.EventHandler<Avalonia.Controls.ApplicationLifetimes.ShutdownRequestedEventArgs>)' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime.remove_ShutdownRequested(System.EventHandler<Avalonia.Controls.ApplicationLifetimes.ShutdownRequestedEventArgs>)' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime.TryShutdown(System.Int32)' is present in the implementation but not in the contract.
CannotRemoveBaseTypeOrInterface : Type 'Avalonia.Controls.Embedding.EmbeddableControlRoot' does not implement interface 'Avalonia.Utilities.IWeakSubscriber<Avalonia.Controls.ResourcesChangedEventArgs>' in the implementation but it does in the contract.
MembersMustExist : Member 'public System.Action<Avalonia.Size> Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.Resized.get()' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public void Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.Resized.set(System.Action<Avalonia.Size>)' does not exist in the implementation but it does exist in the contract.
Expand All @@ -62,4 +63,4 @@ InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platfor
MembersMustExist : Member 'public void Avalonia.Platform.IWindowImpl.Resize(Avalonia.Size)' does not exist in the implementation but it does exist in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IWindowImpl.Resize(Avalonia.Size, Avalonia.Platform.PlatformResizeReason)' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.ITrayIconImpl Avalonia.Platform.IWindowingPlatform.CreateTrayIcon()' is present in the implementation but not in the contract.
Total Issues: 63
Total Issues: 64
Original file line number Diff line number Diff line change
Expand Up @@ -76,36 +76,21 @@ private void HandleWindowClosed(Window window)
return;

if (ShutdownMode == ShutdownMode.OnLastWindowClose && _windows.Count == 0)
Shutdown();
else if (ShutdownMode == ShutdownMode.OnMainWindowClose && window == MainWindow)
Shutdown();
TryShutdown();
else if (ShutdownMode == ShutdownMode.OnMainWindowClose && ReferenceEquals(window, MainWindow))
TryShutdown();
}

public void Shutdown(int exitCode = 0)
{
if (_isShuttingDown)
throw new InvalidOperationException("Application is already shutting down.");

_exitCode = exitCode;
_isShuttingDown = true;
DoShutdown(new ShutdownRequestedEventArgs(), true, exitCode);
}

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 bool TryShutdown(int exitCode = 0)
{
return DoShutdown(new ShutdownRequestedEventArgs(), false, exitCode);
}


public int Start(string[] args)
{
Startup?.Invoke(this, new ControlledApplicationLifetimeStartupEventArgs(args));
Expand All @@ -114,7 +99,10 @@ public int Start(string[] args)

if(options != null && options.ProcessUrlActivationCommandLine && args.Length > 0)
{
((IApplicationPlatformEvents)Application.Current).RaiseUrlsOpened(args);
if (Application.Current is IApplicationPlatformEvents events)
{
events.RaiseUrlsOpened(args);
}
}

var lifetimeEvents = AvaloniaLocator.Current.GetService<IPlatformLifetimeEventsImpl>();
Expand Down Expand Up @@ -145,23 +133,57 @@ public void Dispose()
if (_activeLifetime == this)
_activeLifetime = null;
}
private void OnShutdownRequested(object sender, ShutdownRequestedEventArgs e)

private bool DoShutdown(ShutdownRequestedEventArgs e, bool force = false, int exitCode = 0)
{
ShutdownRequested?.Invoke(this, e);
if (!force)
{
ShutdownRequested?.Invoke(this, e);

if (e.Cancel)
return;
if (e.Cancel)
return false;

if (_isShuttingDown)
throw new InvalidOperationException("Application is already shutting down.");
}

_exitCode = exitCode;
_isShuttingDown = true;

// When an OS shutdown request is received, try to close all non-owned windows. Windows can cancel
// shutdown by setting e.Cancel = true in the Closing event. Owned windows will be shutdown by their
// owners.
foreach (var w in Windows)
if (w.Owner is null)
w.Close();
if (Windows.Count > 0)
e.Cancel = true;
try
{
// When an OS shutdown request is received, try to close all non-owned windows. Windows can cancel
// shutdown by setting e.Cancel = true in the Closing event. Owned windows will be shutdown by their
// owners.
foreach (var w in Windows)
{
if (w.Owner is null)
{
w.Close();
}
}

if (!force && Windows.Count > 0)
{
e.Cancel = true;
return false;
}

var args = new ControlledApplicationLifetimeExitEventArgs(exitCode);
Exit?.Invoke(this, args);
_exitCode = args.ApplicationExitCode;
}
finally
{
_cts?.Cancel();
_cts = null;
_isShuttingDown = false;
}

return true;
}

private void OnShutdownRequested(object sender, ShutdownRequestedEventArgs e) => DoShutdown(e);
}

public class ClassicDesktopStyleApplicationLifetimeOptions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ namespace Avalonia.Controls.ApplicationLifetimes
/// </summary>
public interface IClassicDesktopStyleApplicationLifetime : IControlledApplicationLifetime
{
/// <summary>
/// Tries to Shutdown the application. <see cref="ShutdownRequested" /> event can be used to cancel the shutdown.
/// </summary>
/// <param name="exitCode">An integer exit code for an application. The default exit code is 0.</param>
bool TryShutdown(int exitCode = 0);

/// <summary>
/// Gets the arguments passed to the
/// <see cref="ClassicDesktopStyleApplicationLifetimeExtensions.StartWithClassicDesktopLifetime{T}(T, string[], ShutdownMode)"/>
Expand Down
8 changes: 6 additions & 2 deletions src/Avalonia.Native/AvaloniaNativeMenuExporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -133,9 +133,13 @@ private void PopulateStandardOSXMenuItems(NativeMenu appMenu)
var quitItem = new NativeMenuItem("Quit") { Gesture = new KeyGesture(Key.Q, KeyModifiers.Meta) };
quitItem.Click += (_, _) =>
{
if (Application.Current is { ApplicationLifetime: IControlledApplicationLifetime lifetime })
if (Application.Current is { ApplicationLifetime: IClassicDesktopStyleApplicationLifetime lifetime })
{
lifetime.Shutdown();
lifetime.TryShutdown();
}
else if(Application.Current is {ApplicationLifetime: IControlledApplicationLifetime controlledLifetime})
{
controlledLifetime.Shutdown();
}
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Platform;
using Avalonia.Threading;
using Avalonia.UnitTests;
using Moq;
using Xunit;
Expand Down Expand Up @@ -57,7 +55,7 @@ public void Should_Only_Exit_On_Explicit_Exit()

var hasExit = false;

lifetime.Exit += (s, e) => hasExit = true;
lifetime.Exit += (_, _) => hasExit = true;

var windowA = new Window();

Expand Down Expand Up @@ -91,7 +89,7 @@ public void Should_Exit_After_MainWindow_Closed()

var hasExit = false;

lifetime.Exit += (s, e) => hasExit = true;
lifetime.Exit += (_, _) => hasExit = true;

var mainWindow = new Window();

Expand Down Expand Up @@ -119,7 +117,7 @@ public void Should_Exit_After_Last_Window_Closed()

var hasExit = false;

lifetime.Exit += (s, e) => hasExit = true;
lifetime.Exit += (_, _) => hasExit = true;

var windowA = new Window();

Expand Down Expand Up @@ -226,7 +224,7 @@ public void Should_Allow_Canceling_Shutdown_Via_ShutdownRequested_Event()

window.Show();

lifetime.ShutdownRequested += (s, e) =>
lifetime.ShutdownRequested += (_, e) =>
{
e.Cancel = true;
++raised;
Expand All @@ -238,5 +236,171 @@ public void Should_Allow_Canceling_Shutdown_Via_ShutdownRequested_Event()
Assert.Equal(new[] { window }, lifetime.Windows);
}
}

[Fact]
public void MainWindow_Closed_Shutdown_Should_Be_Cancellable()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
using(var lifetime = new ClassicDesktopStyleApplicationLifetime())
{
lifetime.ShutdownMode = ShutdownMode.OnMainWindowClose;

var hasExit = false;

lifetime.Exit += (_, _) => hasExit = true;

var mainWindow = new Window();

mainWindow.Show();

lifetime.MainWindow = mainWindow;

var window = new Window();

window.Show();

var raised = 0;

lifetime.ShutdownRequested += (_, e) =>
{
e.Cancel = true;
++raised;
};

mainWindow.Close();

Assert.Equal(1, raised);
Assert.False(hasExit);
}
}

[Fact]
public void LastWindow_Closed_Shutdown_Should_Be_Cancellable()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
using(var lifetime = new ClassicDesktopStyleApplicationLifetime())
{
lifetime.ShutdownMode = ShutdownMode.OnLastWindowClose;

var hasExit = false;

lifetime.Exit += (_, _) => hasExit = true;

var windowA = new Window();

windowA.Show();

var windowB = new Window();

windowB.Show();

var raised = 0;

lifetime.ShutdownRequested += (_, e) =>
{
e.Cancel = true;
++raised;
};

windowA.Close();

Assert.False(hasExit);

windowB.Close();

Assert.Equal(1, raised);
Assert.False(hasExit);
}
}

[Fact]
public void TryShutdown_Cancellable_By_Preventing_Window_Close()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
using(var lifetime = new ClassicDesktopStyleApplicationLifetime())
{
var hasExit = false;

lifetime.Exit += (_, _) => hasExit = true;

var windowA = new Window();

windowA.Show();

var windowB = new Window();

windowB.Show();

var raised = 0;

windowA.Closing += (_, e) =>
{
e.Cancel = true;
++raised;
};

lifetime.TryShutdown();

Assert.Equal(1, raised);
Assert.False(hasExit);
}
}

[Fact]
public void Shutdown_NotCancellable_By_Preventing_Window_Close()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
using(var lifetime = new ClassicDesktopStyleApplicationLifetime())
{
var hasExit = false;

lifetime.Exit += (_, _) => hasExit = true;

var windowA = new Window();

windowA.Show();

var windowB = new Window();

windowB.Show();

var raised = 0;

windowA.Closing += (_, e) =>
{
e.Cancel = true;
++raised;
};

lifetime.Shutdown();

Assert.Equal(1, raised);
Assert.True(hasExit);
}
}

[Fact]
public void Shutdown_Doesnt_Raise_Shutdown_Requested()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
using(var lifetime = new ClassicDesktopStyleApplicationLifetime())
{
var hasExit = false;

lifetime.Exit += (_, _) => hasExit = true;

var raised = 0;

lifetime.ShutdownRequested += (_, _) =>
{
++raised;
};

lifetime.Shutdown();

Assert.Equal(0, raised);
Assert.True(hasExit);
}
}
}
}