Skip to content

Commit 6a71056

Browse files
maxkatz6Gillibald
andauthored
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 <[email protected]>
1 parent c391117 commit 6a71056

File tree

5 files changed

+158
-64
lines changed

5 files changed

+158
-64
lines changed

src/Avalonia.Controls/AppBuilder.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using Avalonia.Platform;
77
using Avalonia.Media.Fonts;
88
using Avalonia.Media;
9+
using Avalonia.Metadata;
910

1011
namespace Avalonia
1112
{
@@ -54,6 +55,11 @@ public sealed class AppBuilder
5455
/// </summary>
5556
public Action? RenderingSubsystemInitializer { get; private set; }
5657

58+
/// <summary>
59+
/// Gets a method to override a lifetime factory.
60+
/// </summary>
61+
public Func<Type, IApplicationLifetime?>? LifetimeOverride { get; private set; }
62+
5763
/// <summary>
5864
/// Gets the name of the currently selected rendering subsystem.
5965
/// </summary>
@@ -238,6 +244,13 @@ public AppBuilder UseStandardRuntimePlatformSubsystem()
238244
return Self;
239245
}
240246

247+
[PrivateApi]
248+
public AppBuilder UseLifetimeOverride(Func<Type, IApplicationLifetime?> func)
249+
{
250+
LifetimeOverride = func;
251+
return Self;
252+
}
253+
241254
/// <summary>
242255
/// Configures platform-specific options
243256
/// </summary>

src/Avalonia.Controls/Application.cs

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ public class Application : AvaloniaObject, IDataContextProvider, IGlobalDataTemp
4242
private bool _notifyingResourcesChanged;
4343
private Action<IReadOnlyList<IStyle>>? _stylesAdded;
4444
private Action<IReadOnlyList<IStyle>>? _stylesRemoved;
45+
private IApplicationLifetime? _applicationLifetime;
46+
private bool _setupCompleted;
4547

4648
/// <summary>
4749
/// Defines the <see cref="DataContext"/> property.
@@ -60,7 +62,7 @@ public class Application : AvaloniaObject, IDataContextProvider, IGlobalDataTemp
6062
/// <inheritdoc/>
6163
public event EventHandler<ResourcesChangedEventArgs>? ResourcesChanged;
6264

63-
/// <inheritdoc/>
65+
[Obsolete("Cast ApplicationLifetime to IActivatableApplicationLifetime instead.")]
6466
public event EventHandler<UrlOpenedEventArgs>? UrlsOpened;
6567

6668
/// <inheritdoc/>
@@ -170,15 +172,28 @@ public IResourceDictionary Resources
170172

171173
/// <inheritdoc/>
172174
bool IStyleHost.IsStylesInitialized => _styles != null;
173-
175+
174176
/// <summary>
175177
/// Application lifetime, use it for things like setting the main window and exiting the app from code
176178
/// Currently supported lifetimes are:
177179
/// - <see cref="IClassicDesktopStyleApplicationLifetime"/>
178180
/// - <see cref="ISingleViewApplicationLifetime"/>
179181
/// - <see cref="IControlledApplicationLifetime"/>
182+
/// - <see cref="IActivatableApplicationLifetime"/>
180183
/// </summary>
181-
public IApplicationLifetime? ApplicationLifetime { get; set; }
184+
public IApplicationLifetime? ApplicationLifetime
185+
{
186+
get => _applicationLifetime;
187+
set
188+
{
189+
if (_setupCompleted)
190+
{
191+
throw new InvalidOperationException($"It's not possible to change {nameof(ApplicationLifetime)} after Application was initialized.");
192+
}
193+
194+
_applicationLifetime = value;
195+
}
196+
}
182197

183198
/// <summary>
184199
/// Represents a contract for accessing global platform-specific settings.
@@ -207,7 +222,7 @@ event Action<IReadOnlyList<IStyle>>? IGlobalStyles.GlobalStylesRemoved
207222
/// Initializes the application by loading XAML etc.
208223
/// </summary>
209224
public virtual void Initialize() { }
210-
225+
211226
/// <inheritdoc/>
212227
public bool TryGetResource(object key, ThemeVariant? theme, out object? value)
213228
{
@@ -263,13 +278,15 @@ public virtual void RegisterServices()
263278

264279
AvaloniaLocator.CurrentMutable.Bind<IGlobalClock>()
265280
.ToConstant(MediaContext.Instance.Clock);
281+
282+
_setupCompleted = true;
266283
}
267284

268285
public virtual void OnFrameworkInitializationCompleted()
269286
{
270287
}
271288

272-
void IApplicationPlatformEvents.RaiseUrlsOpened(string[] urls)
289+
void IApplicationPlatformEvents.RaiseUrlsOpened(string[] urls)
273290
{
274291
UrlsOpened?.Invoke(this, new UrlOpenedEventArgs (urls));
275292
}

src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs

Lines changed: 88 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using Avalonia.Controls.ApplicationLifetimes;
99
using Avalonia.Interactivity;
1010
using Avalonia.Platform;
11+
using Avalonia.Reactive;
1112
using Avalonia.Threading;
1213

1314
namespace Avalonia.Controls.ApplicationLifetimes
@@ -18,38 +19,7 @@ public class ClassicDesktopStyleApplicationLifetime : IClassicDesktopStyleApplic
1819
private CancellationTokenSource? _cts;
1920
private bool _isShuttingDown;
2021
private readonly AvaloniaList<Window> _windows = new();
21-
22-
private static ClassicDesktopStyleApplicationLifetime? s_activeLifetime;
23-
24-
static ClassicDesktopStyleApplicationLifetime()
25-
{
26-
Window.WindowOpenedEvent.AddClassHandler(typeof(Window), OnWindowOpened);
27-
Window.WindowClosedEvent.AddClassHandler(typeof(Window), OnWindowClosed);
28-
}
29-
30-
private static void OnWindowClosed(object? sender, RoutedEventArgs e)
31-
{
32-
var window = (Window)sender!;
33-
s_activeLifetime?._windows.Remove(window);
34-
s_activeLifetime?.HandleWindowClosed(window);
35-
}
36-
37-
private static void OnWindowOpened(object? sender, RoutedEventArgs e)
38-
{
39-
var window = (Window)sender!;
40-
if (s_activeLifetime is not null && !s_activeLifetime._windows.Contains(window))
41-
{
42-
s_activeLifetime._windows.Add(window);
43-
}
44-
}
45-
46-
public ClassicDesktopStyleApplicationLifetime()
47-
{
48-
if (s_activeLifetime != null)
49-
throw new InvalidOperationException(
50-
"Can not have multiple active ClassicDesktopStyleApplicationLifetime instances and the previously created one was not disposed");
51-
s_activeLifetime = this;
52-
}
22+
private CompositeDisposable? _compositeDisposable;
5323

5424
/// <inheritdoc/>
5525
public event EventHandler<ControlledApplicationLifetimeStartupEventArgs>? Startup;
@@ -97,9 +67,32 @@ public bool TryShutdown(int exitCode = 0)
9767
{
9868
return DoShutdown(new ShutdownRequestedEventArgs(), true, false, exitCode);
9969
}
100-
101-
public int Start(string[] args)
70+
71+
internal void SetupCore(string[] args)
10272
{
73+
if (_compositeDisposable is not null)
74+
{
75+
// There could be a case, when lifetime was setup without starting.
76+
// Until developer started it manually later. To avoid API breaking changes, it will execute Setup method twice.
77+
return;
78+
}
79+
80+
_compositeDisposable = new CompositeDisposable(
81+
Window.WindowOpenedEvent.AddClassHandler(typeof(Window), (sender, _) =>
82+
{
83+
var window = (Window)sender!;
84+
if (!_windows.Contains(window))
85+
{
86+
_windows.Add(window);
87+
}
88+
}),
89+
Window.WindowClosedEvent.AddClassHandler(typeof(Window), (sender, _) =>
90+
{
91+
var window = (Window)sender!;
92+
_windows.Remove(window);
93+
HandleWindowClosed(window);
94+
}));
95+
10396
Startup?.Invoke(this, new ControlledApplicationLifetimeStartupEventArgs(args));
10497

10598
var options = AvaloniaLocator.Current.GetService<ClassicDesktopStyleApplicationLifetimeOptions>();
@@ -116,9 +109,14 @@ public int Start(string[] args)
116109

117110
if (lifetimeEvents != null)
118111
lifetimeEvents.ShutdownRequested += OnShutdownRequested;
112+
}
119113

120-
_cts = new CancellationTokenSource();
114+
public int Start(string[] args)
115+
{
116+
SetupCore(args);
121117

118+
_cts = new CancellationTokenSource();
119+
122120
// Note due to a bug in the JIT we wrap this in a method, otherwise MainWindow
123121
// gets stuffed into a local var and can not be GCed until after the program stops.
124122
// this method never exits until program end.
@@ -137,8 +135,8 @@ private void ShowMainWindow()
137135

138136
public void Dispose()
139137
{
140-
if (s_activeLifetime == this)
141-
s_activeLifetime = null;
138+
_compositeDisposable?.Dispose();
139+
_compositeDisposable = null;
142140
}
143141

144142
private bool DoShutdown(
@@ -206,21 +204,65 @@ public class ClassicDesktopStyleApplicationLifetimeOptions
206204

207205
namespace Avalonia
208206
{
207+
/// <summary>
208+
/// IClassicDesktopStyleApplicationLifetime related AppBuilder extensions.
209+
/// </summary>
209210
public static class ClassicDesktopStyleApplicationLifetimeExtensions
210211
{
211-
public static int StartWithClassicDesktopLifetime(
212-
this AppBuilder builder, string[] args, ShutdownMode shutdownMode = ShutdownMode.OnLastWindowClose)
212+
private static ClassicDesktopStyleApplicationLifetime PrepareLifetime(AppBuilder builder, string[] args,
213+
Action<IClassicDesktopStyleApplicationLifetime>? lifetimeBuilder)
213214
{
214-
var lifetime = AvaloniaLocator.Current.GetService<ClassicDesktopStyleApplicationLifetime>();
215-
216-
if (lifetime == null)
217-
{
218-
lifetime = new ClassicDesktopStyleApplicationLifetime();
219-
}
215+
var lifetime = builder.LifetimeOverride?.Invoke(typeof(ClassicDesktopStyleApplicationLifetime)) as ClassicDesktopStyleApplicationLifetime
216+
?? new ClassicDesktopStyleApplicationLifetime();
220217

221218
lifetime.Args = args;
222-
lifetime.ShutdownMode = shutdownMode;
219+
lifetimeBuilder?.Invoke(lifetime);
220+
221+
return lifetime;
222+
}
223+
224+
/// <summary>
225+
/// Setups the Application with a IClassicDesktopStyleApplicationLifetime, but doesn't show the main window and doesn't run application main loop.
226+
/// </summary>
227+
/// <param name="builder">Application builder.</param>
228+
/// <param name="args">Startup arguments.</param>
229+
/// <param name="lifetimeBuilder">Lifetime builder to modify the lifetime before application started.</param>
230+
/// <returns>Exit code.</returns>
231+
public static AppBuilder SetupWithClassicDesktopLifetime(this AppBuilder builder, string[] args,
232+
Action<IClassicDesktopStyleApplicationLifetime>? lifetimeBuilder = null)
233+
{
234+
var lifetime = PrepareLifetime(builder, args, lifetimeBuilder);
235+
lifetime.SetupCore(args);
236+
return builder.SetupWithLifetime(lifetime);
237+
}
238+
239+
/// <summary>
240+
/// Starts the Application with a IClassicDesktopStyleApplicationLifetime, shows main window and runs application main loop.
241+
/// </summary>
242+
/// <param name="builder">Application builder.</param>
243+
/// <param name="args">Startup arguments.</param>
244+
/// <param name="lifetimeBuilder">Lifetime builder to modify the lifetime before application started.</param>
245+
/// <returns>Exit code.</returns>
246+
public static int StartWithClassicDesktopLifetime(
247+
this AppBuilder builder, string[] args,
248+
Action<IClassicDesktopStyleApplicationLifetime>? lifetimeBuilder = null)
249+
{
250+
var lifetime = PrepareLifetime(builder, args, lifetimeBuilder);
251+
builder.SetupWithLifetime(lifetime);
252+
return lifetime.Start(args);
253+
}
223254

255+
/// <summary>
256+
/// Starts the Application with a IClassicDesktopStyleApplicationLifetime, shows main window and runs application main loop.
257+
/// </summary>
258+
/// <param name="builder">Application builder.</param>
259+
/// <param name="args">Startup arguments.</param>
260+
/// <param name="shutdownMode">Lifetime shutdown mode.</param>
261+
/// <returns>Exit code.</returns>
262+
public static int StartWithClassicDesktopLifetime(
263+
this AppBuilder builder, string[] args, ShutdownMode shutdownMode)
264+
{
265+
var lifetime = PrepareLifetime(builder, args, l => l.ShutdownMode = shutdownMode);
224266
builder.SetupWithLifetime(lifetime);
225267
return lifetime.Start(args);
226268
}

src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,20 +13,19 @@ public static AppBuilder UseAvaloniaNative(this AppBuilder builder)
1313
builder
1414
.UseStandardRuntimePlatformSubsystem()
1515
.UseWindowingSubsystem(() =>
16-
{
17-
var platform = AvaloniaNativePlatform.Initialize(
18-
AvaloniaLocator.Current.GetService<AvaloniaNativePlatformOptions>() ??
19-
new AvaloniaNativePlatformOptions());
16+
{
17+
var platform = AvaloniaNativePlatform.Initialize(
18+
AvaloniaLocator.Current.GetService<AvaloniaNativePlatformOptions>() ??
19+
new AvaloniaNativePlatformOptions());
2020

21-
builder.AfterSetup (x=>
22-
{
23-
platform.SetupApplicationName();
24-
platform.SetupApplicationMenuExporter();
25-
});
26-
});
27-
28-
AvaloniaLocator.CurrentMutable.Bind<ClassicDesktopStyleApplicationLifetime>()
29-
.ToConstant(new MacOSClassicDesktopStyleApplicationLifetime());
21+
builder.AfterSetup (x=>
22+
{
23+
platform.SetupApplicationName();
24+
platform.SetupApplicationMenuExporter();
25+
});
26+
})
27+
.UseLifetimeOverride(type => type == typeof(ClassicDesktopStyleApplicationLifetime)
28+
? new MacOSClassicDesktopStyleApplicationLifetime() : null);
3029

3130
return builder;
3231
}

0 commit comments

Comments
 (0)