diff --git a/src/Avalonia.Controls/AppBuilder.cs b/src/Avalonia.Controls/AppBuilder.cs
index ab9802a134..9aa24afd16 100644
--- a/src/Avalonia.Controls/AppBuilder.cs
+++ b/src/Avalonia.Controls/AppBuilder.cs
@@ -353,12 +353,21 @@ namespace Avalonia
SetupUnsafe();
}
+ ///
+ /// Allows to be called again after it has already been called once.
+ ///
+ internal static void ResetSetupForUnitTests()
+ => s_setupWasAlreadyCalled = false;
+
///
/// Setup method that doesn't check for input initalizers being set.
/// Nor
///
internal void SetupUnsafe()
{
+ var setupLifetime = _lifetime as ISetupApplicationLifetime;
+ setupLifetime?.BeforeAppInit();
+
_optionsInitializers?.Invoke();
RuntimePlatformServicesInitializer?.Invoke();
TextShapingSubsystemInitializer?.Invoke();
@@ -373,6 +382,8 @@ namespace Avalonia
AfterApplicationSetupCallback?.Invoke(Self);
AfterSetupCallback?.Invoke(Self);
Instance.OnFrameworkInitializationCompleted();
+
+ setupLifetime?.AfterAppInit();
}
}
}
diff --git a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs
index 8d509832fa..741a6380bc 100644
--- a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs
+++ b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Threading;
using Avalonia.Collections;
@@ -12,13 +13,19 @@ using Avalonia.Threading;
namespace Avalonia.Controls.ApplicationLifetimes
{
- public class ClassicDesktopStyleApplicationLifetime : IClassicDesktopStyleApplicationLifetime, IDisposable
+ public class ClassicDesktopStyleApplicationLifetime :
+ IClassicDesktopStyleApplicationLifetime,
+ ISetupApplicationLifetime,
+ IDisposable
{
private int _exitCode;
private CancellationTokenSource? _cts;
private bool _isShuttingDown;
private readonly AvaloniaList _windows = new();
- private CompositeDisposable? _compositeDisposable;
+ private CompositeDisposable? _globalEventsSubscriptions;
+ private bool _beforeInitCalled;
+ private bool _afterInitCalled;
+ private IPlatformLifetimeEventsImpl? _platformLifetimeEventsImpl;
///
public event EventHandler? Startup;
@@ -67,16 +74,11 @@ namespace Avalonia.Controls.ApplicationLifetimes
return DoShutdown(new ShutdownRequestedEventArgs(), true, false, exitCode);
}
- internal void SubscribeGlobalEvents()
+ private void SubscribeGlobalEvents()
{
- if (_compositeDisposable is not null)
- {
- // There could be a case, when lifetime was setup without starting.
- // Until developer started it manually later. To avoid API breaking changes, it will execute Setup method twice.
- return;
- }
+ Debug.Assert(_globalEventsSubscriptions is null);
- _compositeDisposable = new CompositeDisposable(
+ _globalEventsSubscriptions = new CompositeDisposable(
Window.WindowOpenedEvent.AddClassHandler(typeof(Window), (sender, _) =>
{
var window = (Window)sender!;
@@ -93,16 +95,32 @@ namespace Avalonia.Controls.ApplicationLifetimes
}));
}
- internal void SetupCore(string[] args)
+ private void BeforeInit()
{
+ if (_beforeInitCalled)
+ return;
+
+ _beforeInitCalled = true;
SubscribeGlobalEvents();
+ }
- Startup?.Invoke(this, new ControlledApplicationLifetimeStartupEventArgs(args));
+ void ISetupApplicationLifetime.BeforeAppInit()
+ => BeforeInit();
- var lifetimeEvents = AvaloniaLocator.Current.GetService();
+ private void AfterInit()
+ {
+ if (_afterInitCalled)
+ return;
- if (lifetimeEvents != null)
- lifetimeEvents.ShutdownRequested += OnShutdownRequested;
+ _afterInitCalled = true;
+
+ _platformLifetimeEventsImpl = AvaloniaLocator.Current.GetService();
+ _platformLifetimeEventsImpl?.ShutdownRequested += OnShutdownRequested;
+ }
+
+ void ISetupApplicationLifetime.AfterAppInit()
+ {
+ AfterInit();
}
public int Start(string[] args)
@@ -121,7 +139,11 @@ namespace Avalonia.Controls.ApplicationLifetimes
internal int StartCore(string[] args)
{
- SetupCore(args);
+ // Before/AfterInit should have been called from ISetupApplicationLifetime.
+ // If somehow they weren't (e.g., for a manually started lifetime), do it now.
+ BeforeInit();
+ AfterInit();
+ Startup?.Invoke(this, new ControlledApplicationLifetimeStartupEventArgs(args));
_cts = new CancellationTokenSource();
@@ -143,8 +165,11 @@ namespace Avalonia.Controls.ApplicationLifetimes
public void Dispose()
{
- _compositeDisposable?.Dispose();
- _compositeDisposable = null;
+ _globalEventsSubscriptions?.Dispose();
+ _globalEventsSubscriptions = null;
+
+ _platformLifetimeEventsImpl?.ShutdownRequested -= OnShutdownRequested;
+ _platformLifetimeEventsImpl = null;
}
private bool DoShutdown(
@@ -222,15 +247,12 @@ namespace Avalonia
///
public static class ClassicDesktopStyleApplicationLifetimeExtensions
{
- private static ClassicDesktopStyleApplicationLifetime PrepareLifetime(AppBuilder builder, string[] args,
+ private static ClassicDesktopStyleApplicationLifetime CreateLifetime(
+ string[] args,
Action? lifetimeBuilder)
{
- var lifetime = new ClassicDesktopStyleApplicationLifetime();
- lifetime.SubscribeGlobalEvents();
-
- lifetime.Args = args;
+ var lifetime = new ClassicDesktopStyleApplicationLifetime { Args = args };
lifetimeBuilder?.Invoke(lifetime);
-
return lifetime;
}
@@ -244,8 +266,7 @@ namespace Avalonia
public static AppBuilder SetupWithClassicDesktopLifetime(this AppBuilder builder, string[] args,
Action? lifetimeBuilder = null)
{
- var lifetime = PrepareLifetime(builder, args, lifetimeBuilder);
- lifetime.SetupCore(args);
+ var lifetime = CreateLifetime(args, lifetimeBuilder);
return builder.SetupWithLifetime(lifetime);
}
@@ -260,7 +281,7 @@ namespace Avalonia
this AppBuilder builder, string[] args,
Action? lifetimeBuilder = null)
{
- var lifetime = PrepareLifetime(builder, args, lifetimeBuilder);
+ var lifetime = CreateLifetime(args, lifetimeBuilder);
builder.SetupWithLifetime(lifetime);
return lifetime.Start(args);
}
@@ -275,7 +296,7 @@ namespace Avalonia
public static int StartWithClassicDesktopLifetime(
this AppBuilder builder, string[] args, ShutdownMode shutdownMode)
{
- var lifetime = PrepareLifetime(builder, args, l => l.ShutdownMode = shutdownMode);
+ var lifetime = CreateLifetime(args, l => l.ShutdownMode = shutdownMode);
builder.SetupWithLifetime(lifetime);
return lifetime.Start(args);
}
diff --git a/src/Avalonia.Controls/ApplicationLifetimes/ISetupApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/ISetupApplicationLifetime.cs
new file mode 100644
index 0000000000..25ce4410d2
--- /dev/null
+++ b/src/Avalonia.Controls/ApplicationLifetimes/ISetupApplicationLifetime.cs
@@ -0,0 +1,17 @@
+namespace Avalonia.Controls.ApplicationLifetimes;
+
+///
+/// An interface for lifetimes that need to execute extra code before and after initialization.
+///
+internal interface ISetupApplicationLifetime
+{
+ ///
+ /// Called before anything is initialized: platforms, rendering, app, etc. aren't available yet.
+ ///
+ void BeforeAppInit();
+
+ ///
+ /// Called after the app has been initialized.
+ ///
+ void AfterAppInit();
+}
diff --git a/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs b/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs
index 9ae80dbacb..05282a6ba8 100644
--- a/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs
@@ -1,11 +1,7 @@
using System;
using System.Collections.Generic;
-using System.Threading;
using Avalonia.Controls.ApplicationLifetimes;
-using Avalonia.Controls.Platform;
using Avalonia.Platform;
-using Avalonia.Rendering;
-using Avalonia.Rendering.Composition;
using Avalonia.Threading;
using Avalonia.UnitTests;
using Moq;
@@ -13,25 +9,15 @@ using Xunit;
namespace Avalonia.Controls.UnitTests
{
-
public class DesktopStyleApplicationLifetimeTests : ScopedTestBase
{
- IDispatcherImpl CreateDispatcherWithInstantMainLoop()
- {
- var mock = new Mock();
- mock.Setup(x => x.RunLoop(It.IsAny()))
- .Callback(() => Dispatcher.UIThread.ExitAllFrames());
- mock.Setup(x => x.CurrentThreadIsLoopThread).Returns(true);
- return mock.Object;
- }
-
[Fact]
public void Should_Set_ExitCode_After_Shutdown()
{
using (UnitTestApplication.Start(new TestServices()))
using(var lifetime = new ClassicDesktopStyleApplicationLifetime())
{
- lifetime.SetupCore(Array.Empty());
+ Setup(lifetime);
Dispatcher.UIThread.Post(() => lifetime.Shutdown(1337));
var exitCode = lifetime.Start(Array.Empty());
@@ -47,7 +33,7 @@ namespace Avalonia.Controls.UnitTests
using (UnitTestApplication.Start(TestServices.StyledWindow))
using(var lifetime = new ClassicDesktopStyleApplicationLifetime())
{
- lifetime.SetupCore(Array.Empty());
+ Setup(lifetime);
var windows = new List { new Window(), new Window(), new Window(), new Window() };
@@ -69,7 +55,7 @@ namespace Avalonia.Controls.UnitTests
using(var lifetime = new ClassicDesktopStyleApplicationLifetime())
{
lifetime.ShutdownMode = ShutdownMode.OnExplicitShutdown;
- lifetime.SetupCore(Array.Empty());
+ Setup(lifetime);
var hasExit = false;
@@ -104,7 +90,7 @@ namespace Avalonia.Controls.UnitTests
using(var lifetime = new ClassicDesktopStyleApplicationLifetime())
{
lifetime.ShutdownMode = ShutdownMode.OnMainWindowClose;
- lifetime.SetupCore(Array.Empty());
+ Setup(lifetime);
var hasExit = false;
@@ -133,7 +119,7 @@ namespace Avalonia.Controls.UnitTests
using(var lifetime = new ClassicDesktopStyleApplicationLifetime())
{
lifetime.ShutdownMode = ShutdownMode.OnMainWindowClose;
- lifetime.SetupCore(Array.Empty());
+ Setup(lifetime);
var hasExit = false;
var secondaryWindowClosingExecuted = false;
@@ -173,7 +159,7 @@ namespace Avalonia.Controls.UnitTests
using(var lifetime = new ClassicDesktopStyleApplicationLifetime())
{
lifetime.ShutdownMode = ShutdownMode.OnMainWindowClose;
- lifetime.SetupCore(Array.Empty());
+ Setup(lifetime);
var hasExit = false;
var secondaryWindowClosingExecuted = false;
@@ -213,7 +199,7 @@ namespace Avalonia.Controls.UnitTests
using(var lifetime = new ClassicDesktopStyleApplicationLifetime())
{
lifetime.ShutdownMode = ShutdownMode.OnLastWindowClose;
- lifetime.SetupCore(Array.Empty());
+ Setup(lifetime);
var hasExit = false;
@@ -243,7 +229,7 @@ namespace Avalonia.Controls.UnitTests
using (UnitTestApplication.Start(TestServices.StyledWindow))
using(var lifetime = new ClassicDesktopStyleApplicationLifetime())
{
- lifetime.SetupCore(Array.Empty());
+ Setup(lifetime);
var window = new Window();
@@ -259,7 +245,7 @@ namespace Avalonia.Controls.UnitTests
using (UnitTestApplication.Start(TestServices.StyledWindow))
using(var lifetime = new ClassicDesktopStyleApplicationLifetime())
{
- lifetime.SetupCore(Array.Empty());
+ Setup(lifetime);
var window = new Window();
@@ -279,7 +265,7 @@ namespace Avalonia.Controls.UnitTests
using (UnitTestApplication.Start(TestServices.StyledWindow))
using(var lifetime = new ClassicDesktopStyleApplicationLifetime())
{
- lifetime.SetupCore(Array.Empty());
+ Setup(lifetime);
var window = new Window();
@@ -311,7 +297,7 @@ namespace Avalonia.Controls.UnitTests
using (UnitTestApplication.Start(services))
using(var lifetime = new ClassicDesktopStyleApplicationLifetime())
{
- lifetime.SetupCore(Array.Empty());
+ Setup(lifetime);
var window = new Window();
@@ -361,7 +347,7 @@ namespace Avalonia.Controls.UnitTests
using(var lifetime = new ClassicDesktopStyleApplicationLifetime())
{
lifetime.ShutdownMode = ShutdownMode.OnMainWindowClose;
- lifetime.SetupCore(Array.Empty());
+ Setup(lifetime);
var hasExit = false;
@@ -399,7 +385,7 @@ namespace Avalonia.Controls.UnitTests
using(var lifetime = new ClassicDesktopStyleApplicationLifetime())
{
lifetime.ShutdownMode = ShutdownMode.OnLastWindowClose;
- lifetime.SetupCore(Array.Empty());
+ Setup(lifetime);
var hasExit = false;
@@ -438,7 +424,7 @@ namespace Avalonia.Controls.UnitTests
using (UnitTestApplication.Start(TestServices.StyledWindow))
using(var lifetime = new ClassicDesktopStyleApplicationLifetime())
{
- lifetime.SetupCore(Array.Empty());
+ Setup(lifetime);
lifetime.Exit += (_, _) => Assert.Fail("lifetime.Exit was called.");
Dispatcher.UIThread.ShutdownStarted += UiThreadOnShutdownStarted;
@@ -478,7 +464,7 @@ namespace Avalonia.Controls.UnitTests
using (UnitTestApplication.Start(TestServices.StyledWindow.With()))
using(var lifetime = new ClassicDesktopStyleApplicationLifetime())
{
- lifetime.SetupCore(Array.Empty());
+ Setup(lifetime);
var hasExit = false;
var closingRaised = 0;
@@ -518,7 +504,7 @@ namespace Avalonia.Controls.UnitTests
using (UnitTestApplication.Start(TestServices.StyledWindow))
using(var lifetime = new ClassicDesktopStyleApplicationLifetime())
{
- lifetime.SetupCore(Array.Empty());
+ Setup(lifetime);
var hasExit = false;
@@ -537,5 +523,130 @@ namespace Avalonia.Controls.UnitTests
Assert.True(hasExit);
}
}
+
+ [Fact]
+ public void SetupWithClassicDesktopLifetime_Should_Subscribe_To_Platform_ShutdownRequested()
+ {
+ var lifetimeEvents = new Mock();
+ ClassicDesktopStyleApplicationLifetime? lifetime = null;
+
+ CreateAppBuilder(lifetimeEvents.Object).SetupWithClassicDesktopLifetime(
+ [],
+ l => lifetime = (ClassicDesktopStyleApplicationLifetime)l);
+
+ Assert.NotNull(lifetime);
+
+ using (lifetime)
+ {
+ var window = new Window();
+ window.Show();
+
+ var raised = 0;
+
+ lifetime.ShutdownRequested += (_, e) =>
+ {
+ e.Cancel = true;
+ ++raised;
+ };
+
+ lifetimeEvents.Raise(x => x.ShutdownRequested += null, new ShutdownRequestedEventArgs());
+
+ Assert.Equal(1, raised);
+ Assert.Equal([window], lifetime.Windows);
+ }
+ }
+
+ [Fact]
+ public void SetupWithClassicDesktopLifetime_Should_Not_Raise_Startup()
+ {
+ var frameworkInitCalled = false;
+ var lifetimeBuilderCalled = false;
+ var startupRaised = false;
+
+ CreateAppBuilder(onFrameworkInitializationCompleted: () => frameworkInitCalled = true)
+ .SetupWithClassicDesktopLifetime(
+ ["foo", "bar"],
+ l =>
+ {
+ lifetimeBuilderCalled = true;
+ l.Startup += (_, _) => startupRaised = true;
+ });
+
+ Assert.True(frameworkInitCalled);
+ Assert.True(lifetimeBuilderCalled);
+ Assert.False(startupRaised);
+ }
+
+ [Fact]
+ public void Start_After_SetupWithClassicDesktopLifetime_Should_Not_Raise_Startup_Twice()
+ {
+ ClassicDesktopStyleApplicationLifetime? lifetime = null;
+ var raised = 0;
+
+ CreateAppBuilder().SetupWithClassicDesktopLifetime(
+ [],
+ l =>
+ {
+ lifetime = (ClassicDesktopStyleApplicationLifetime)l;
+ l.Startup += (_, _) => ++raised;
+ });
+
+ Assert.NotNull(lifetime);
+
+ using (lifetime)
+ {
+ Assert.Equal(0, raised);
+
+ Dispatcher.UIThread.Post(Dispatcher.UIThread.ExitAllFrames);
+ lifetime.Start([]);
+
+ Assert.Equal(1, raised);
+ }
+ }
+
+ private static void Setup(ClassicDesktopStyleApplicationLifetime lifetime)
+ {
+ ISetupApplicationLifetime setupLifetime = lifetime;
+ setupLifetime.BeforeAppInit();
+ setupLifetime.AfterAppInit();
+ }
+
+ private static AppBuilder CreateAppBuilder(
+ IPlatformLifetimeEventsImpl? platformLifetimeEvents = null,
+ Action? onFrameworkInitializationCompleted = null)
+ {
+ AppBuilder.ResetSetupForUnitTests();
+
+ return AppBuilder.Configure(() => new SetupTestApplication(onFrameworkInitializationCompleted))
+ .UseRuntimePlatformSubsystem(() => { })
+ .UseRenderingSubsystem(() => { })
+ .UseTextShapingSubsystem(() => { })
+ .UseWindowingSubsystem(() =>
+ {
+ if (platformLifetimeEvents is not null)
+ AvaloniaLocator.CurrentMutable.Bind().ToConstant(platformLifetimeEvents);
+ });
+ }
+
+ private sealed class SetupTestApplication(Action? onFrameworkInitializationCompleted)
+ : UnitTestApplication(TestServices.StyledWindow)
+ {
+ private bool _servicesRegistered;
+
+ public override void RegisterServices()
+ {
+ if (_servicesRegistered)
+ return;
+
+ _servicesRegistered = true;
+ base.RegisterServices();
+ }
+
+ public override void OnFrameworkInitializationCompleted()
+ {
+ base.OnFrameworkInitializationCompleted();
+ onFrameworkInitializationCompleted?.Invoke();
+ }
+ }
}
}