Browse Source

Make Classic ApplicationLifetime API a bit more reliable (#14267)

* Add StartWithClassicDesktopLifetime overload with a lifetime builder

* Disallow changing Application.ApplicationLifetime after setup was completed

* Avoid static dependency on a singleton lifetime

* Introduce SetupWithClassicDesktopLifetime method

* Move more logic from Start method to Setup

* Add docs

* Avoid public API changes

* Fix tests

* Repalce locator usage with `.UseLifetimeOverride`

---------

Co-authored-by: Benedikt Stebner <Gillibald@users.noreply.github.com>
pull/14303/head
Max Katz 2 years ago
committed by GitHub
parent
commit
6a71056686
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 13
      src/Avalonia.Controls/AppBuilder.cs
  2. 27
      src/Avalonia.Controls/Application.cs
  3. 134
      src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs
  4. 25
      src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs
  5. 23
      tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs

13
src/Avalonia.Controls/AppBuilder.cs

@ -6,6 +6,7 @@ using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Platform;
using Avalonia.Media.Fonts;
using Avalonia.Media;
using Avalonia.Metadata;
namespace Avalonia
{
@ -54,6 +55,11 @@ namespace Avalonia
/// </summary>
public Action? RenderingSubsystemInitializer { get; private set; }
/// <summary>
/// Gets a method to override a lifetime factory.
/// </summary>
public Func<Type, IApplicationLifetime?>? LifetimeOverride { get; private set; }
/// <summary>
/// Gets the name of the currently selected rendering subsystem.
/// </summary>
@ -238,6 +244,13 @@ namespace Avalonia
return Self;
}
[PrivateApi]
public AppBuilder UseLifetimeOverride(Func<Type, IApplicationLifetime?> func)
{
LifetimeOverride = func;
return Self;
}
/// <summary>
/// Configures platform-specific options
/// </summary>

27
src/Avalonia.Controls/Application.cs

@ -42,6 +42,8 @@ namespace Avalonia
private bool _notifyingResourcesChanged;
private Action<IReadOnlyList<IStyle>>? _stylesAdded;
private Action<IReadOnlyList<IStyle>>? _stylesRemoved;
private IApplicationLifetime? _applicationLifetime;
private bool _setupCompleted;
/// <summary>
/// Defines the <see cref="DataContext"/> property.
@ -60,7 +62,7 @@ namespace Avalonia
/// <inheritdoc/>
public event EventHandler<ResourcesChangedEventArgs>? ResourcesChanged;
/// <inheritdoc/>
[Obsolete("Cast ApplicationLifetime to IActivatableApplicationLifetime instead.")]
public event EventHandler<UrlOpenedEventArgs>? UrlsOpened;
/// <inheritdoc/>
@ -170,15 +172,28 @@ namespace Avalonia
/// <inheritdoc/>
bool IStyleHost.IsStylesInitialized => _styles != null;
/// <summary>
/// Application lifetime, use it for things like setting the main window and exiting the app from code
/// Currently supported lifetimes are:
/// - <see cref="IClassicDesktopStyleApplicationLifetime"/>
/// - <see cref="ISingleViewApplicationLifetime"/>
/// - <see cref="IControlledApplicationLifetime"/>
/// - <see cref="IActivatableApplicationLifetime"/>
/// </summary>
public IApplicationLifetime? ApplicationLifetime { get; set; }
public IApplicationLifetime? ApplicationLifetime
{
get => _applicationLifetime;
set
{
if (_setupCompleted)
{
throw new InvalidOperationException($"It's not possible to change {nameof(ApplicationLifetime)} after Application was initialized.");
}
_applicationLifetime = value;
}
}
/// <summary>
/// Represents a contract for accessing global platform-specific settings.
@ -207,7 +222,7 @@ namespace Avalonia
/// Initializes the application by loading XAML etc.
/// </summary>
public virtual void Initialize() { }
/// <inheritdoc/>
public bool TryGetResource(object key, ThemeVariant? theme, out object? value)
{
@ -263,13 +278,15 @@ namespace Avalonia
AvaloniaLocator.CurrentMutable.Bind<IGlobalClock>()
.ToConstant(MediaContext.Instance.Clock);
_setupCompleted = true;
}
public virtual void OnFrameworkInitializationCompleted()
{
}
void IApplicationPlatformEvents.RaiseUrlsOpened(string[] urls)
void IApplicationPlatformEvents.RaiseUrlsOpened(string[] urls)
{
UrlsOpened?.Invoke(this, new UrlOpenedEventArgs (urls));
}

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

@ -8,6 +8,7 @@ using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Interactivity;
using Avalonia.Platform;
using Avalonia.Reactive;
using Avalonia.Threading;
namespace Avalonia.Controls.ApplicationLifetimes
@ -18,38 +19,7 @@ namespace Avalonia.Controls.ApplicationLifetimes
private CancellationTokenSource? _cts;
private bool _isShuttingDown;
private readonly AvaloniaList<Window> _windows = new();
private static ClassicDesktopStyleApplicationLifetime? s_activeLifetime;
static ClassicDesktopStyleApplicationLifetime()
{
Window.WindowOpenedEvent.AddClassHandler(typeof(Window), OnWindowOpened);
Window.WindowClosedEvent.AddClassHandler(typeof(Window), OnWindowClosed);
}
private static void OnWindowClosed(object? sender, RoutedEventArgs e)
{
var window = (Window)sender!;
s_activeLifetime?._windows.Remove(window);
s_activeLifetime?.HandleWindowClosed(window);
}
private static void OnWindowOpened(object? sender, RoutedEventArgs e)
{
var window = (Window)sender!;
if (s_activeLifetime is not null && !s_activeLifetime._windows.Contains(window))
{
s_activeLifetime._windows.Add(window);
}
}
public ClassicDesktopStyleApplicationLifetime()
{
if (s_activeLifetime != null)
throw new InvalidOperationException(
"Can not have multiple active ClassicDesktopStyleApplicationLifetime instances and the previously created one was not disposed");
s_activeLifetime = this;
}
private CompositeDisposable? _compositeDisposable;
/// <inheritdoc/>
public event EventHandler<ControlledApplicationLifetimeStartupEventArgs>? Startup;
@ -97,9 +67,32 @@ namespace Avalonia.Controls.ApplicationLifetimes
{
return DoShutdown(new ShutdownRequestedEventArgs(), true, false, exitCode);
}
public int Start(string[] args)
internal void SetupCore(string[] args)
{
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;
}
_compositeDisposable = new CompositeDisposable(
Window.WindowOpenedEvent.AddClassHandler(typeof(Window), (sender, _) =>
{
var window = (Window)sender!;
if (!_windows.Contains(window))
{
_windows.Add(window);
}
}),
Window.WindowClosedEvent.AddClassHandler(typeof(Window), (sender, _) =>
{
var window = (Window)sender!;
_windows.Remove(window);
HandleWindowClosed(window);
}));
Startup?.Invoke(this, new ControlledApplicationLifetimeStartupEventArgs(args));
var options = AvaloniaLocator.Current.GetService<ClassicDesktopStyleApplicationLifetimeOptions>();
@ -116,9 +109,14 @@ namespace Avalonia.Controls.ApplicationLifetimes
if (lifetimeEvents != null)
lifetimeEvents.ShutdownRequested += OnShutdownRequested;
}
_cts = new CancellationTokenSource();
public int Start(string[] args)
{
SetupCore(args);
_cts = new CancellationTokenSource();
// Note due to a bug in the JIT we wrap this in a method, otherwise MainWindow
// gets stuffed into a local var and can not be GCed until after the program stops.
// this method never exits until program end.
@ -137,8 +135,8 @@ namespace Avalonia.Controls.ApplicationLifetimes
public void Dispose()
{
if (s_activeLifetime == this)
s_activeLifetime = null;
_compositeDisposable?.Dispose();
_compositeDisposable = null;
}
private bool DoShutdown(
@ -206,21 +204,65 @@ namespace Avalonia.Controls.ApplicationLifetimes
namespace Avalonia
{
/// <summary>
/// IClassicDesktopStyleApplicationLifetime related AppBuilder extensions.
/// </summary>
public static class ClassicDesktopStyleApplicationLifetimeExtensions
{
public static int StartWithClassicDesktopLifetime(
this AppBuilder builder, string[] args, ShutdownMode shutdownMode = ShutdownMode.OnLastWindowClose)
private static ClassicDesktopStyleApplicationLifetime PrepareLifetime(AppBuilder builder, string[] args,
Action<IClassicDesktopStyleApplicationLifetime>? lifetimeBuilder)
{
var lifetime = AvaloniaLocator.Current.GetService<ClassicDesktopStyleApplicationLifetime>();
if (lifetime == null)
{
lifetime = new ClassicDesktopStyleApplicationLifetime();
}
var lifetime = builder.LifetimeOverride?.Invoke(typeof(ClassicDesktopStyleApplicationLifetime)) as ClassicDesktopStyleApplicationLifetime
?? new ClassicDesktopStyleApplicationLifetime();
lifetime.Args = args;
lifetime.ShutdownMode = shutdownMode;
lifetimeBuilder?.Invoke(lifetime);
return lifetime;
}
/// <summary>
/// Setups the Application with a IClassicDesktopStyleApplicationLifetime, but doesn't show the main window and doesn't run application main loop.
/// </summary>
/// <param name="builder">Application builder.</param>
/// <param name="args">Startup arguments.</param>
/// <param name="lifetimeBuilder">Lifetime builder to modify the lifetime before application started.</param>
/// <returns>Exit code.</returns>
public static AppBuilder SetupWithClassicDesktopLifetime(this AppBuilder builder, string[] args,
Action<IClassicDesktopStyleApplicationLifetime>? lifetimeBuilder = null)
{
var lifetime = PrepareLifetime(builder, args, lifetimeBuilder);
lifetime.SetupCore(args);
return builder.SetupWithLifetime(lifetime);
}
/// <summary>
/// Starts the Application with a IClassicDesktopStyleApplicationLifetime, shows main window and runs application main loop.
/// </summary>
/// <param name="builder">Application builder.</param>
/// <param name="args">Startup arguments.</param>
/// <param name="lifetimeBuilder">Lifetime builder to modify the lifetime before application started.</param>
/// <returns>Exit code.</returns>
public static int StartWithClassicDesktopLifetime(
this AppBuilder builder, string[] args,
Action<IClassicDesktopStyleApplicationLifetime>? lifetimeBuilder = null)
{
var lifetime = PrepareLifetime(builder, args, lifetimeBuilder);
builder.SetupWithLifetime(lifetime);
return lifetime.Start(args);
}
/// <summary>
/// Starts the Application with a IClassicDesktopStyleApplicationLifetime, shows main window and runs application main loop.
/// </summary>
/// <param name="builder">Application builder.</param>
/// <param name="args">Startup arguments.</param>
/// <param name="shutdownMode">Lifetime shutdown mode.</param>
/// <returns>Exit code.</returns>
public static int StartWithClassicDesktopLifetime(
this AppBuilder builder, string[] args, ShutdownMode shutdownMode)
{
var lifetime = PrepareLifetime(builder, args, l => l.ShutdownMode = shutdownMode);
builder.SetupWithLifetime(lifetime);
return lifetime.Start(args);
}

25
src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs

@ -13,20 +13,19 @@ namespace Avalonia
builder
.UseStandardRuntimePlatformSubsystem()
.UseWindowingSubsystem(() =>
{
var platform = AvaloniaNativePlatform.Initialize(
AvaloniaLocator.Current.GetService<AvaloniaNativePlatformOptions>() ??
new AvaloniaNativePlatformOptions());
{
var platform = AvaloniaNativePlatform.Initialize(
AvaloniaLocator.Current.GetService<AvaloniaNativePlatformOptions>() ??
new AvaloniaNativePlatformOptions());
builder.AfterSetup (x=>
{
platform.SetupApplicationName();
platform.SetupApplicationMenuExporter();
});
});
AvaloniaLocator.CurrentMutable.Bind<ClassicDesktopStyleApplicationLifetime>()
.ToConstant(new MacOSClassicDesktopStyleApplicationLifetime());
builder.AfterSetup (x=>
{
platform.SetupApplicationName();
platform.SetupApplicationMenuExporter();
});
})
.UseLifetimeOverride(type => type == typeof(ClassicDesktopStyleApplicationLifetime)
? new MacOSClassicDesktopStyleApplicationLifetime() : null);
return builder;
}

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

@ -31,6 +31,8 @@ namespace Avalonia.Controls.UnitTests
using (UnitTestApplication.Start(new TestServices(dispatcherImpl: new ManagedDispatcherImpl(null))))
using(var lifetime = new ClassicDesktopStyleApplicationLifetime())
{
lifetime.SetupCore(Array.Empty<string>());
Dispatcher.UIThread.Post(() => lifetime.Shutdown(1337));
var exitCode = lifetime.Start(Array.Empty<string>());
@ -45,6 +47,8 @@ namespace Avalonia.Controls.UnitTests
using (UnitTestApplication.Start(TestServices.StyledWindow))
using(var lifetime = new ClassicDesktopStyleApplicationLifetime())
{
lifetime.SetupCore(Array.Empty<string>());
var windows = new List<Window> { new Window(), new Window(), new Window(), new Window() };
foreach (var window in windows)
@ -65,6 +69,7 @@ namespace Avalonia.Controls.UnitTests
using(var lifetime = new ClassicDesktopStyleApplicationLifetime())
{
lifetime.ShutdownMode = ShutdownMode.OnExplicitShutdown;
lifetime.SetupCore(Array.Empty<string>());
var hasExit = false;
@ -99,6 +104,7 @@ namespace Avalonia.Controls.UnitTests
using(var lifetime = new ClassicDesktopStyleApplicationLifetime())
{
lifetime.ShutdownMode = ShutdownMode.OnMainWindowClose;
lifetime.SetupCore(Array.Empty<string>());
var hasExit = false;
@ -127,6 +133,7 @@ namespace Avalonia.Controls.UnitTests
using(var lifetime = new ClassicDesktopStyleApplicationLifetime())
{
lifetime.ShutdownMode = ShutdownMode.OnLastWindowClose;
lifetime.SetupCore(Array.Empty<string>());
var hasExit = false;
@ -156,6 +163,8 @@ namespace Avalonia.Controls.UnitTests
using (UnitTestApplication.Start(TestServices.StyledWindow))
using(var lifetime = new ClassicDesktopStyleApplicationLifetime())
{
lifetime.SetupCore(Array.Empty<string>());
var window = new Window();
window.Show();
@ -170,6 +179,8 @@ namespace Avalonia.Controls.UnitTests
using (UnitTestApplication.Start(TestServices.StyledWindow))
using(var lifetime = new ClassicDesktopStyleApplicationLifetime())
{
lifetime.SetupCore(Array.Empty<string>());
var window = new Window();
window.Show();
@ -188,6 +199,8 @@ namespace Avalonia.Controls.UnitTests
using (UnitTestApplication.Start(TestServices.StyledWindow))
using(var lifetime = new ClassicDesktopStyleApplicationLifetime())
{
lifetime.SetupCore(Array.Empty<string>());
var window = new Window();
window.Show();
@ -213,6 +226,8 @@ namespace Avalonia.Controls.UnitTests
using (UnitTestApplication.Start(services))
using(var lifetime = new ClassicDesktopStyleApplicationLifetime())
{
lifetime.SetupCore(Array.Empty<string>());
var window = new Window();
window.Show();
@ -261,6 +276,7 @@ namespace Avalonia.Controls.UnitTests
using(var lifetime = new ClassicDesktopStyleApplicationLifetime())
{
lifetime.ShutdownMode = ShutdownMode.OnMainWindowClose;
lifetime.SetupCore(Array.Empty<string>());
var hasExit = false;
@ -298,6 +314,7 @@ namespace Avalonia.Controls.UnitTests
using(var lifetime = new ClassicDesktopStyleApplicationLifetime())
{
lifetime.ShutdownMode = ShutdownMode.OnLastWindowClose;
lifetime.SetupCore(Array.Empty<string>());
var hasExit = false;
@ -336,6 +353,8 @@ namespace Avalonia.Controls.UnitTests
using (UnitTestApplication.Start(TestServices.StyledWindow))
using(var lifetime = new ClassicDesktopStyleApplicationLifetime())
{
lifetime.SetupCore(Array.Empty<string>());
var hasExit = false;
lifetime.Exit += (_, _) => hasExit = true;
@ -369,6 +388,8 @@ namespace Avalonia.Controls.UnitTests
using (UnitTestApplication.Start(TestServices.StyledWindow.With(dispatcherImpl: CreateDispatcherWithInstantMainLoop())))
using(var lifetime = new ClassicDesktopStyleApplicationLifetime())
{
lifetime.SetupCore(Array.Empty<string>());
var hasExit = false;
lifetime.Exit += (_, _) => hasExit = true;
@ -402,6 +423,8 @@ namespace Avalonia.Controls.UnitTests
using (UnitTestApplication.Start(TestServices.StyledWindow))
using(var lifetime = new ClassicDesktopStyleApplicationLifetime())
{
lifetime.SetupCore(Array.Empty<string>());
var hasExit = false;
lifetime.Exit += (_, _) => hasExit = true;

Loading…
Cancel
Save