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();
}
/// <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>
/// Setup method that doesn't check for input initalizers being set.
/// Nor
/// </summary>
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();
}
}
}

77
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<Window> _windows = new();
private CompositeDisposable? _compositeDisposable;
private CompositeDisposable? _globalEventsSubscriptions;
private bool _beforeInitCalled;
private bool _afterInitCalled;
private IPlatformLifetimeEventsImpl? _platformLifetimeEventsImpl;
/// <inheritdoc/>
public event EventHandler<ControlledApplicationLifetimeStartupEventArgs>? 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<IPlatformLifetimeEventsImpl>();
private void AfterInit()
{
if (_afterInitCalled)
return;
if (lifetimeEvents != null)
lifetimeEvents.ShutdownRequested += OnShutdownRequested;
_afterInitCalled = true;
_platformLifetimeEventsImpl = AvaloniaLocator.Current.GetService<IPlatformLifetimeEventsImpl>();
_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
/// </summary>
public static class ClassicDesktopStyleApplicationLifetimeExtensions
{
private static ClassicDesktopStyleApplicationLifetime PrepareLifetime(AppBuilder builder, string[] args,
private static ClassicDesktopStyleApplicationLifetime CreateLifetime(
string[] args,
Action<IClassicDesktopStyleApplicationLifetime>? 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<IClassicDesktopStyleApplicationLifetime>? 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<IClassicDesktopStyleApplicationLifetime>? 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);
}

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.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<IControlledDispatcherImpl>();
mock.Setup(x => x.RunLoop(It.IsAny<CancellationToken>()))
.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<string>());
Setup(lifetime);
Dispatcher.UIThread.Post(() => lifetime.Shutdown(1337));
var exitCode = lifetime.Start(Array.Empty<string>());
@ -47,7 +33,7 @@ namespace Avalonia.Controls.UnitTests
using (UnitTestApplication.Start(TestServices.StyledWindow))
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() };
@ -69,7 +55,7 @@ namespace Avalonia.Controls.UnitTests
using(var lifetime = new ClassicDesktopStyleApplicationLifetime())
{
lifetime.ShutdownMode = ShutdownMode.OnExplicitShutdown;
lifetime.SetupCore(Array.Empty<string>());
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<string>());
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<string>());
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<string>());
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<string>());
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<string>());
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<string>());
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<string>());
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<string>());
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<string>());
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<string>());
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<string>());
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<string>());
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<string>());
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<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