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(); + } + } } }