Browse Source

Fix init order of SetupWithClassicDesktopLifetime (#21954)

* Fix init order of SetupWithClassicDesktopLifetime

* Don't raise Startup in Setup

* Fix warning
pull/22129/head
Julien Lebosquain 4 weeks ago
committed by GitHub
parent
commit
5d25bd6463
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 11
      src/Avalonia.Controls/AppBuilder.cs
  2. 77
      src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs
  3. 17
      src/Avalonia.Controls/ApplicationLifetimes/ISetupApplicationLifetime.cs
  4. 171
      tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs

11
src/Avalonia.Controls/AppBuilder.cs

@ -353,12 +353,21 @@ namespace Avalonia
SetupUnsafe(); SetupUnsafe();
} }
/// <summary>
/// Allows <see cref="Setup"/> to be called again after it has already been called once.
/// </summary>
internal static void ResetSetupForUnitTests()
=> s_setupWasAlreadyCalled = false;
/// <summary> /// <summary>
/// Setup method that doesn't check for input initalizers being set. /// Setup method that doesn't check for input initalizers being set.
/// Nor /// Nor
/// </summary> /// </summary>
internal void SetupUnsafe() internal void SetupUnsafe()
{ {
var setupLifetime = _lifetime as ISetupApplicationLifetime;
setupLifetime?.BeforeAppInit();
_optionsInitializers?.Invoke(); _optionsInitializers?.Invoke();
RuntimePlatformServicesInitializer?.Invoke(); RuntimePlatformServicesInitializer?.Invoke();
TextShapingSubsystemInitializer?.Invoke(); TextShapingSubsystemInitializer?.Invoke();
@ -373,6 +382,8 @@ namespace Avalonia
AfterApplicationSetupCallback?.Invoke(Self); AfterApplicationSetupCallback?.Invoke(Self);
AfterSetupCallback?.Invoke(Self); AfterSetupCallback?.Invoke(Self);
Instance.OnFrameworkInitializationCompleted(); Instance.OnFrameworkInitializationCompleted();
setupLifetime?.AfterAppInit();
} }
} }
} }

77
src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Threading; using System.Threading;
using Avalonia.Collections; using Avalonia.Collections;
@ -12,13 +13,19 @@ using Avalonia.Threading;
namespace Avalonia.Controls.ApplicationLifetimes namespace Avalonia.Controls.ApplicationLifetimes
{ {
public class ClassicDesktopStyleApplicationLifetime : IClassicDesktopStyleApplicationLifetime, IDisposable public class ClassicDesktopStyleApplicationLifetime :
IClassicDesktopStyleApplicationLifetime,
ISetupApplicationLifetime,
IDisposable
{ {
private int _exitCode; private int _exitCode;
private CancellationTokenSource? _cts; private CancellationTokenSource? _cts;
private bool _isShuttingDown; private bool _isShuttingDown;
private readonly AvaloniaList<Window> _windows = new(); private readonly AvaloniaList<Window> _windows = new();
private CompositeDisposable? _compositeDisposable; private CompositeDisposable? _globalEventsSubscriptions;
private bool _beforeInitCalled;
private bool _afterInitCalled;
private IPlatformLifetimeEventsImpl? _platformLifetimeEventsImpl;
/// <inheritdoc/> /// <inheritdoc/>
public event EventHandler<ControlledApplicationLifetimeStartupEventArgs>? Startup; public event EventHandler<ControlledApplicationLifetimeStartupEventArgs>? Startup;
@ -67,16 +74,11 @@ namespace Avalonia.Controls.ApplicationLifetimes
return DoShutdown(new ShutdownRequestedEventArgs(), true, false, exitCode); return DoShutdown(new ShutdownRequestedEventArgs(), true, false, exitCode);
} }
internal void SubscribeGlobalEvents() private void SubscribeGlobalEvents()
{ {
if (_compositeDisposable is not null) Debug.Assert(_globalEventsSubscriptions is 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;
}
_compositeDisposable = new CompositeDisposable( _globalEventsSubscriptions = new CompositeDisposable(
Window.WindowOpenedEvent.AddClassHandler(typeof(Window), (sender, _) => Window.WindowOpenedEvent.AddClassHandler(typeof(Window), (sender, _) =>
{ {
var window = (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(); SubscribeGlobalEvents();
}
Startup?.Invoke(this, new ControlledApplicationLifetimeStartupEventArgs(args)); void ISetupApplicationLifetime.BeforeAppInit()
=> BeforeInit();
var lifetimeEvents = AvaloniaLocator.Current.GetService<IPlatformLifetimeEventsImpl>(); private void AfterInit()
{
if (_afterInitCalled)
return;
if (lifetimeEvents != null) _afterInitCalled = true;
lifetimeEvents.ShutdownRequested += OnShutdownRequested;
_platformLifetimeEventsImpl = AvaloniaLocator.Current.GetService<IPlatformLifetimeEventsImpl>();
_platformLifetimeEventsImpl?.ShutdownRequested += OnShutdownRequested;
}
void ISetupApplicationLifetime.AfterAppInit()
{
AfterInit();
} }
public int Start(string[] args) public int Start(string[] args)
@ -121,7 +139,11 @@ namespace Avalonia.Controls.ApplicationLifetimes
internal int StartCore(string[] args) 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(); _cts = new CancellationTokenSource();
@ -143,8 +165,11 @@ namespace Avalonia.Controls.ApplicationLifetimes
public void Dispose() public void Dispose()
{ {
_compositeDisposable?.Dispose(); _globalEventsSubscriptions?.Dispose();
_compositeDisposable = null; _globalEventsSubscriptions = null;
_platformLifetimeEventsImpl?.ShutdownRequested -= OnShutdownRequested;
_platformLifetimeEventsImpl = null;
} }
private bool DoShutdown( private bool DoShutdown(
@ -222,15 +247,12 @@ namespace Avalonia
/// </summary> /// </summary>
public static class ClassicDesktopStyleApplicationLifetimeExtensions public static class ClassicDesktopStyleApplicationLifetimeExtensions
{ {
private static ClassicDesktopStyleApplicationLifetime PrepareLifetime(AppBuilder builder, string[] args, private static ClassicDesktopStyleApplicationLifetime CreateLifetime(
string[] args,
Action<IClassicDesktopStyleApplicationLifetime>? lifetimeBuilder) Action<IClassicDesktopStyleApplicationLifetime>? lifetimeBuilder)
{ {
var lifetime = new ClassicDesktopStyleApplicationLifetime(); var lifetime = new ClassicDesktopStyleApplicationLifetime { Args = args };
lifetime.SubscribeGlobalEvents();
lifetime.Args = args;
lifetimeBuilder?.Invoke(lifetime); lifetimeBuilder?.Invoke(lifetime);
return lifetime; return lifetime;
} }
@ -244,8 +266,7 @@ namespace Avalonia
public static AppBuilder SetupWithClassicDesktopLifetime(this AppBuilder builder, string[] args, public static AppBuilder SetupWithClassicDesktopLifetime(this AppBuilder builder, string[] args,
Action<IClassicDesktopStyleApplicationLifetime>? lifetimeBuilder = null) Action<IClassicDesktopStyleApplicationLifetime>? lifetimeBuilder = null)
{ {
var lifetime = PrepareLifetime(builder, args, lifetimeBuilder); var lifetime = CreateLifetime(args, lifetimeBuilder);
lifetime.SetupCore(args);
return builder.SetupWithLifetime(lifetime); return builder.SetupWithLifetime(lifetime);
} }
@ -260,7 +281,7 @@ namespace Avalonia
this AppBuilder builder, string[] args, this AppBuilder builder, string[] args,
Action<IClassicDesktopStyleApplicationLifetime>? lifetimeBuilder = null) Action<IClassicDesktopStyleApplicationLifetime>? lifetimeBuilder = null)
{ {
var lifetime = PrepareLifetime(builder, args, lifetimeBuilder); var lifetime = CreateLifetime(args, lifetimeBuilder);
builder.SetupWithLifetime(lifetime); builder.SetupWithLifetime(lifetime);
return lifetime.Start(args); return lifetime.Start(args);
} }
@ -275,7 +296,7 @@ namespace Avalonia
public static int StartWithClassicDesktopLifetime( public static int StartWithClassicDesktopLifetime(
this AppBuilder builder, string[] args, ShutdownMode shutdownMode) 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); builder.SetupWithLifetime(lifetime);
return lifetime.Start(args); return lifetime.Start(args);
} }

17
src/Avalonia.Controls/ApplicationLifetimes/ISetupApplicationLifetime.cs

@ -0,0 +1,17 @@
namespace Avalonia.Controls.ApplicationLifetimes;
/// <summary>
/// An interface for lifetimes that need to execute extra code before and after initialization.
/// </summary>
internal interface ISetupApplicationLifetime
{
/// <summary>
/// Called before anything is initialized: platforms, rendering, app, etc. aren't available yet.
/// </summary>
void BeforeAppInit();
/// <summary>
/// Called after the app has been initialized.
/// </summary>
void AfterAppInit();
}

171
tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs

@ -1,11 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading;
using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Controls.Platform;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.Rendering.Composition;
using Avalonia.Threading; using Avalonia.Threading;
using Avalonia.UnitTests; using Avalonia.UnitTests;
using Moq; using Moq;
@ -13,25 +9,15 @@ using Xunit;
namespace Avalonia.Controls.UnitTests namespace Avalonia.Controls.UnitTests
{ {
public class DesktopStyleApplicationLifetimeTests : ScopedTestBase public class DesktopStyleApplicationLifetimeTests : ScopedTestBase
{ {
IDispatcherImpl CreateDispatcherWithInstantMainLoop()
{
var mock = new Mock<IControlledDispatcherImpl>();
mock.Setup(x => x.RunLoop(It.IsAny<CancellationToken>()))
.Callback(() => Dispatcher.UIThread.ExitAllFrames());
mock.Setup(x => x.CurrentThreadIsLoopThread).Returns(true);
return mock.Object;
}
[Fact] [Fact]
public void Should_Set_ExitCode_After_Shutdown() public void Should_Set_ExitCode_After_Shutdown()
{ {
using (UnitTestApplication.Start(new TestServices())) using (UnitTestApplication.Start(new TestServices()))
using(var lifetime = new ClassicDesktopStyleApplicationLifetime()) using(var lifetime = new ClassicDesktopStyleApplicationLifetime())
{ {
lifetime.SetupCore(Array.Empty<string>()); Setup(lifetime);
Dispatcher.UIThread.Post(() => lifetime.Shutdown(1337)); Dispatcher.UIThread.Post(() => lifetime.Shutdown(1337));
var exitCode = lifetime.Start(Array.Empty<string>()); var exitCode = lifetime.Start(Array.Empty<string>());
@ -47,7 +33,7 @@ namespace Avalonia.Controls.UnitTests
using (UnitTestApplication.Start(TestServices.StyledWindow)) using (UnitTestApplication.Start(TestServices.StyledWindow))
using(var lifetime = new ClassicDesktopStyleApplicationLifetime()) using(var lifetime = new ClassicDesktopStyleApplicationLifetime())
{ {
lifetime.SetupCore(Array.Empty<string>()); Setup(lifetime);
var windows = new List<Window> { new Window(), new Window(), new Window(), new Window() }; var windows = new List<Window> { new Window(), new Window(), new Window(), new Window() };
@ -69,7 +55,7 @@ namespace Avalonia.Controls.UnitTests
using(var lifetime = new ClassicDesktopStyleApplicationLifetime()) using(var lifetime = new ClassicDesktopStyleApplicationLifetime())
{ {
lifetime.ShutdownMode = ShutdownMode.OnExplicitShutdown; lifetime.ShutdownMode = ShutdownMode.OnExplicitShutdown;
lifetime.SetupCore(Array.Empty<string>()); Setup(lifetime);
var hasExit = false; var hasExit = false;
@ -104,7 +90,7 @@ namespace Avalonia.Controls.UnitTests
using(var lifetime = new ClassicDesktopStyleApplicationLifetime()) using(var lifetime = new ClassicDesktopStyleApplicationLifetime())
{ {
lifetime.ShutdownMode = ShutdownMode.OnMainWindowClose; lifetime.ShutdownMode = ShutdownMode.OnMainWindowClose;
lifetime.SetupCore(Array.Empty<string>()); Setup(lifetime);
var hasExit = false; var hasExit = false;
@ -133,7 +119,7 @@ namespace Avalonia.Controls.UnitTests
using(var lifetime = new ClassicDesktopStyleApplicationLifetime()) using(var lifetime = new ClassicDesktopStyleApplicationLifetime())
{ {
lifetime.ShutdownMode = ShutdownMode.OnMainWindowClose; lifetime.ShutdownMode = ShutdownMode.OnMainWindowClose;
lifetime.SetupCore(Array.Empty<string>()); Setup(lifetime);
var hasExit = false; var hasExit = false;
var secondaryWindowClosingExecuted = false; var secondaryWindowClosingExecuted = false;
@ -173,7 +159,7 @@ namespace Avalonia.Controls.UnitTests
using(var lifetime = new ClassicDesktopStyleApplicationLifetime()) using(var lifetime = new ClassicDesktopStyleApplicationLifetime())
{ {
lifetime.ShutdownMode = ShutdownMode.OnMainWindowClose; lifetime.ShutdownMode = ShutdownMode.OnMainWindowClose;
lifetime.SetupCore(Array.Empty<string>()); Setup(lifetime);
var hasExit = false; var hasExit = false;
var secondaryWindowClosingExecuted = false; var secondaryWindowClosingExecuted = false;
@ -213,7 +199,7 @@ namespace Avalonia.Controls.UnitTests
using(var lifetime = new ClassicDesktopStyleApplicationLifetime()) using(var lifetime = new ClassicDesktopStyleApplicationLifetime())
{ {
lifetime.ShutdownMode = ShutdownMode.OnLastWindowClose; lifetime.ShutdownMode = ShutdownMode.OnLastWindowClose;
lifetime.SetupCore(Array.Empty<string>()); Setup(lifetime);
var hasExit = false; var hasExit = false;
@ -243,7 +229,7 @@ namespace Avalonia.Controls.UnitTests
using (UnitTestApplication.Start(TestServices.StyledWindow)) using (UnitTestApplication.Start(TestServices.StyledWindow))
using(var lifetime = new ClassicDesktopStyleApplicationLifetime()) using(var lifetime = new ClassicDesktopStyleApplicationLifetime())
{ {
lifetime.SetupCore(Array.Empty<string>()); Setup(lifetime);
var window = new Window(); var window = new Window();
@ -259,7 +245,7 @@ namespace Avalonia.Controls.UnitTests
using (UnitTestApplication.Start(TestServices.StyledWindow)) using (UnitTestApplication.Start(TestServices.StyledWindow))
using(var lifetime = new ClassicDesktopStyleApplicationLifetime()) using(var lifetime = new ClassicDesktopStyleApplicationLifetime())
{ {
lifetime.SetupCore(Array.Empty<string>()); Setup(lifetime);
var window = new Window(); var window = new Window();
@ -279,7 +265,7 @@ namespace Avalonia.Controls.UnitTests
using (UnitTestApplication.Start(TestServices.StyledWindow)) using (UnitTestApplication.Start(TestServices.StyledWindow))
using(var lifetime = new ClassicDesktopStyleApplicationLifetime()) using(var lifetime = new ClassicDesktopStyleApplicationLifetime())
{ {
lifetime.SetupCore(Array.Empty<string>()); Setup(lifetime);
var window = new Window(); var window = new Window();
@ -311,7 +297,7 @@ namespace Avalonia.Controls.UnitTests
using (UnitTestApplication.Start(services)) using (UnitTestApplication.Start(services))
using(var lifetime = new ClassicDesktopStyleApplicationLifetime()) using(var lifetime = new ClassicDesktopStyleApplicationLifetime())
{ {
lifetime.SetupCore(Array.Empty<string>()); Setup(lifetime);
var window = new Window(); var window = new Window();
@ -361,7 +347,7 @@ namespace Avalonia.Controls.UnitTests
using(var lifetime = new ClassicDesktopStyleApplicationLifetime()) using(var lifetime = new ClassicDesktopStyleApplicationLifetime())
{ {
lifetime.ShutdownMode = ShutdownMode.OnMainWindowClose; lifetime.ShutdownMode = ShutdownMode.OnMainWindowClose;
lifetime.SetupCore(Array.Empty<string>()); Setup(lifetime);
var hasExit = false; var hasExit = false;
@ -399,7 +385,7 @@ namespace Avalonia.Controls.UnitTests
using(var lifetime = new ClassicDesktopStyleApplicationLifetime()) using(var lifetime = new ClassicDesktopStyleApplicationLifetime())
{ {
lifetime.ShutdownMode = ShutdownMode.OnLastWindowClose; lifetime.ShutdownMode = ShutdownMode.OnLastWindowClose;
lifetime.SetupCore(Array.Empty<string>()); Setup(lifetime);
var hasExit = false; var hasExit = false;
@ -438,7 +424,7 @@ namespace Avalonia.Controls.UnitTests
using (UnitTestApplication.Start(TestServices.StyledWindow)) using (UnitTestApplication.Start(TestServices.StyledWindow))
using(var lifetime = new ClassicDesktopStyleApplicationLifetime()) using(var lifetime = new ClassicDesktopStyleApplicationLifetime())
{ {
lifetime.SetupCore(Array.Empty<string>()); Setup(lifetime);
lifetime.Exit += (_, _) => Assert.Fail("lifetime.Exit was called."); lifetime.Exit += (_, _) => Assert.Fail("lifetime.Exit was called.");
Dispatcher.UIThread.ShutdownStarted += UiThreadOnShutdownStarted; Dispatcher.UIThread.ShutdownStarted += UiThreadOnShutdownStarted;
@ -478,7 +464,7 @@ namespace Avalonia.Controls.UnitTests
using (UnitTestApplication.Start(TestServices.StyledWindow.With())) using (UnitTestApplication.Start(TestServices.StyledWindow.With()))
using(var lifetime = new ClassicDesktopStyleApplicationLifetime()) using(var lifetime = new ClassicDesktopStyleApplicationLifetime())
{ {
lifetime.SetupCore(Array.Empty<string>()); Setup(lifetime);
var hasExit = false; var hasExit = false;
var closingRaised = 0; var closingRaised = 0;
@ -518,7 +504,7 @@ namespace Avalonia.Controls.UnitTests
using (UnitTestApplication.Start(TestServices.StyledWindow)) using (UnitTestApplication.Start(TestServices.StyledWindow))
using(var lifetime = new ClassicDesktopStyleApplicationLifetime()) using(var lifetime = new ClassicDesktopStyleApplicationLifetime())
{ {
lifetime.SetupCore(Array.Empty<string>()); Setup(lifetime);
var hasExit = false; var hasExit = false;
@ -537,5 +523,130 @@ namespace Avalonia.Controls.UnitTests
Assert.True(hasExit); Assert.True(hasExit);
} }
} }
[Fact]
public void SetupWithClassicDesktopLifetime_Should_Subscribe_To_Platform_ShutdownRequested()
{
var lifetimeEvents = new Mock<IPlatformLifetimeEventsImpl>();
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<IPlatformLifetimeEventsImpl>().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();
}
}
} }
} }

Loading…
Cancel
Save