Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Switch to ISingleViewApplicationLifetime #269

Merged
merged 14 commits into from
Jan 20, 2025
24 changes: 15 additions & 9 deletions src/Consolonia.Blazor/BuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,26 +26,32 @@ public static AppBuilder UseConsoloniaBlazor(this AppBuilder builder,
sc.AddSingleton(_ =>
{
var lifetime =
(IClassicDesktopStyleApplicationLifetime)Application.Current?.ApplicationLifetime;
(ConsoloniaLifetime)Application.Current?.ApplicationLifetime;
ArgumentNullException.ThrowIfNull(lifetime);
return lifetime;
});
sc.AddSingleton(sp =>
(ISingleViewApplicationLifetime)sp.GetRequiredService<ConsoloniaLifetime>()
);
sc.AddSingleton(sp =>
(IControlledApplicationLifetime)sp.GetRequiredService<ConsoloniaLifetime>()
);
sc.AddTransient(sp =>
sp.GetRequiredService<IClassicDesktopStyleApplicationLifetime>().MainWindow?.StorageProvider);
sp.GetRequiredService<ConsoloniaLifetime>().TopLevel?.StorageProvider);
sc.AddTransient(sp =>
sp.GetRequiredService<IClassicDesktopStyleApplicationLifetime>().MainWindow?.Clipboard);
sp.GetRequiredService<ConsoloniaLifetime>().TopLevel?.Clipboard);
sc.AddTransient(sp =>
sp.GetRequiredService<IClassicDesktopStyleApplicationLifetime>().MainWindow?.InsetsManager);
sp.GetRequiredService<ConsoloniaLifetime>().TopLevel?.InsetsManager);
sc.AddTransient(sp =>
sp.GetRequiredService<IClassicDesktopStyleApplicationLifetime>().MainWindow?.InputPane);
sp.GetRequiredService<ConsoloniaLifetime>().TopLevel?.InputPane);
sc.AddTransient(sp =>
sp.GetRequiredService<IClassicDesktopStyleApplicationLifetime>().MainWindow?.Launcher);
sp.GetRequiredService<ConsoloniaLifetime>().TopLevel?.Launcher);
sc.AddTransient(sp =>
sp.GetRequiredService<IClassicDesktopStyleApplicationLifetime>().MainWindow?.Screens);
sp.GetRequiredService<ConsoloniaLifetime>().TopLevel?.Screens);
sc.AddTransient(sp =>
sp.GetRequiredService<IClassicDesktopStyleApplicationLifetime>().MainWindow?.FocusManager);
sp.GetRequiredService<ConsoloniaLifetime>().TopLevel?.FocusManager);
sc.AddTransient(sp =>
sp.GetRequiredService<IClassicDesktopStyleApplicationLifetime>().MainWindow?.PlatformSettings);
sp.GetRequiredService<ConsoloniaLifetime>().TopLevel?.PlatformSettings);

if (configureServices != null) configureServices(sc);
});
Expand Down
61 changes: 43 additions & 18 deletions src/Consolonia.Core/ApplicationStartup.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
Expand All @@ -13,6 +14,7 @@
// ReSharper disable CheckNamespace
// ReSharper disable MemberCanBePrivate.Global

#nullable enable
namespace Consolonia
{
public static class ApplicationStartup
Expand All @@ -25,7 +27,7 @@ public static class ApplicationStartup
public static void StartConsolonia<TApp>(IConsole console, IConsoleColorMode consoleColorMode,
params string[] args) where TApp : Application, new()
{
ClassicDesktopStyleApplicationLifetime lifetime = BuildLifetime<TApp>(console, consoleColorMode, args);
ConsoloniaLifetime lifetime = BuildLifetime<TApp>(console, consoleColorMode, args);

lifetime.Start(args);
}
Expand Down Expand Up @@ -60,7 +62,7 @@ public static AppBuilder UseConsolonia(this AppBuilder builder)
}, nameof(ConsoloniaRenderInterface));
}

public static ClassicDesktopStyleApplicationLifetime BuildLifetime<TApp>(IConsole console,
public static ConsoloniaLifetime BuildLifetime<TApp>(IConsole console,
IConsoleColorMode consoleColorMode, string[] args)
where TApp : Application, new()
{
Expand All @@ -73,32 +75,55 @@ public static ClassicDesktopStyleApplicationLifetime BuildLifetime<TApp>(IConsol
return CreateLifetime(consoloniaAppBuilder, args);
}

private static ClassicDesktopStyleApplicationLifetime CreateLifetime(AppBuilder builder, string[] args)
/// <summary>
/// Shuts down the application with the specified exit code.
/// </summary>
/// <param name="lifetime">The application lifetime.</param>
/// <param name="exitCode">The exit code to use.</param>
/// <exception cref="InvalidOperationException">Thrown when the lifetime does not support controlled shutdown.</exception>
public static void Shutdown(this IApplicationLifetime lifetime, int exitCode = 0)
{
if (lifetime is IControlledApplicationLifetime controlledLifetime)
controlledLifetime.Shutdown(exitCode);
else
throw new InvalidOperationException("The lifetime does not support controlled shutdown.");
}

/// <summary>
/// Shuts down the application with the specified exit code.
/// </summary>
/// <param name="lifetime">The application lifetime.</param>
/// <param name="exitCode">The exit code to use.</param>
/// <exception cref="InvalidOperationException">Thrown when the lifetime does not support controlled shutdown.</exception>
public static void TryShutdown(this IApplicationLifetime lifetime, int exitCode = 0)
{
if (lifetime is IControlledApplicationLifetime controlledLifetime)
controlledLifetime.TryShutdown(exitCode);
else
throw new InvalidOperationException("The lifetime does not support controlled shutdown.");
}

private static ConsoloniaLifetime CreateLifetime(AppBuilder builder, string[] args)
{
var lifetime = new ConsoloniaLifetime
{
Args = args,
ShutdownMode = ShutdownMode.OnMainWindowClose
Args = args
};

builder.SetupWithLifetime(lifetime);

// Application has been instantiated here.
// We need to initialize it

// override AccessText to use ConsoloniaAccessText as default ContentPresenter for unknown data types (aka string)
Application.Current.DataTemplates.Add(new FuncDataTemplate<object>(
(data, _) =>
Application.Current!.DataTemplates.Add(new FuncDataTemplate<object>(
(_, _) =>
{
if (data != null)
{
var result = new ConsoloniaAccessText();
// ReSharper disable AccessToStaticMemberViaDerivedType
result.Bind(TextBlock.TextProperty,
result.GetBindingObservable(Control.DataContextProperty, x => x?.ToString()));
return result;
}

return null;
var result = new ConsoloniaAccessText();
// ReSharper disable AccessToStaticMemberViaDerivedType
result.Bind(TextBlock.TextProperty,
result.GetBindingObservable(Control.DataContextProperty, x => x?.ToString()));
return result;
},
true)
);
Expand All @@ -109,7 +134,7 @@ private static ClassicDesktopStyleApplicationLifetime CreateLifetime(AppBuilder
public static int StartWithConsoleLifetime(
this AppBuilder builder, string[] args)
{
ClassicDesktopStyleApplicationLifetime lifetime = CreateLifetime(builder, args);
ConsoloniaLifetime lifetime = CreateLifetime(builder, args);
return lifetime.Start(args);
}
}
Expand Down
217 changes: 217 additions & 0 deletions src/Consolonia.Core/ConsoloniaLifetime.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Platform;
using Avalonia.Threading;
using Consolonia.Core.Drawing.PixelBufferImplementation;
using Consolonia.Core.Infrastructure;

// ReSharper disable CheckNamespace
// ReSharper disable NotNullOrRequiredMemberIsNotInitialized
// ReSharper disable ConstantConditionalAccessQualifier
namespace Consolonia
{
public class ConsoloniaLifetime : ISingleViewApplicationLifetime,
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

May be we should propose Avalonia to split their implementation of single view lifetime into core packages just to re-use that among platforms. Sounds meaningful?

IControlledApplicationLifetime,
ISingleTopLevelApplicationLifetime,
IDisposable
{
private CancellationTokenSource _cts = new();
private bool _disposedValue;
private int _exitCode;
private bool _isShuttingDown;

/// <summary>
/// Gets the arguments passed to the AppBuilder Start method.
/// </summary>
#pragma warning disable CA1819 // Properties should not return arrays
public string[] Args { get; set; }
#pragma warning restore CA1819 // Properties should not return arrays

public event EventHandler<ControlledApplicationLifetimeStartupEventArgs> Startup;

public event EventHandler<ControlledApplicationLifetimeExitEventArgs> Exit;

public void Shutdown(int exitCode = 0)
{
DoShutdown(new ShutdownRequestedEventArgs(), true, true, exitCode);
}

public TopLevel TopLevel { get; set; }

public Control MainView
{
get => (Control)TopLevel.Content;
tomlm marked this conversation as resolved.
Show resolved Hide resolved
set
{
if (TopLevel == null) TopLevel = new Window();
TopLevel.Content = value;
}
}

public event EventHandler<ShutdownRequestedEventArgs> ShutdownRequested;

public bool TryShutdown(int exitCode = 0)
{
return DoShutdown(new ShutdownRequestedEventArgs(), true, false, exitCode);
}

internal void SetupCore(string[] args)
{
Startup?.Invoke(this, new ControlledApplicationLifetimeStartupEventArgs(args));

var lifetimeEvents = AvaloniaLocator.Current.GetService<IPlatformLifetimeEventsImpl>();

if (lifetimeEvents != null)
tomlm marked this conversation as resolved.
Show resolved Hide resolved
lifetimeEvents.ShutdownRequested += OnShutdownRequested;

TopLevel.Closed += (_, _) => TryShutdown();
}

public int Start(string[] args)
{
return StartCore(args);
}

/// <summary>
/// Since the lifetime must be set up/prepared with 'args' before executing Start(), an overload with no parameters
/// seems more suitable for integrating with some lifetime manager providers, such as MS HostApplicationBuilder.
/// </summary>
/// <returns>exit code</returns>
public int Start()
{
return StartCore(Args ?? Array.Empty<string>());
}

internal int StartCore(string[] args)
{
SetupCore(args);

tomlm marked this conversation as resolved.
Show resolved Hide resolved
(TopLevel as Window)?.Show();

Dispatcher.UIThread.MainLoop(_cts.Token);
Environment.ExitCode = _exitCode;
return _exitCode;
} // ReSharper disable UnusedParameter.Local
// ReSharper disable UnusedMember.Local
#pragma warning disable IDE0060 // Remove unused parameter
private bool DoShutdown(
ShutdownRequestedEventArgs e,
bool isProgrammatic,
bool force = false,
int exitCode = 0)
{
if (!force)
{
ShutdownRequested?.Invoke(this, e);

if (e.Cancel)
return false;

if (_isShuttingDown)
throw new InvalidOperationException("Application is already shutting down.");
}

_exitCode = exitCode;
_isShuttingDown = true;

var consoleWindow = (ConsoleWindow)TopLevel.PlatformImpl;
consoleWindow.Console.RestoreConsole();

tomlm marked this conversation as resolved.
Show resolved Hide resolved
try
{
var args = new ControlledApplicationLifetimeExitEventArgs(exitCode);
Exit?.Invoke(this, args);
_exitCode = args.ApplicationExitCode;
}
finally
{
_isShuttingDown = false;

_cts?.Cancel();
_cts = null;
Dispatcher.UIThread.InvokeShutdown();
}

return true;
}
#pragma warning restore IDE0060 // Remove unused parameter

// ReSharper disable once UnusedMember.Local
private void OnShutdownRequested(object sender, ShutdownRequestedEventArgs e)
{
DoShutdown(e, false);
}

/// <summary>
/// returned task indicates that console is successfully paused
/// </summary>
public Task DisconnectFromConsoleAsync(CancellationToken cancellationToken)
{
var taskToWaitFor = new TaskCompletionSource();
cancellationToken.Register(() => taskToWaitFor.SetResult());

var mainWindowPlatformImpl = (ConsoleWindow)TopLevel.PlatformImpl;
IConsole console = mainWindowPlatformImpl!.Console;

tomlm marked this conversation as resolved.
Show resolved Hide resolved
Task pauseTask = taskToWaitFor.Task;

console.PauseIO(pauseTask);

pauseTask.ContinueWith(_ =>
{
mainWindowPlatformImpl.Console.ClearScreen();

Dispatcher.UIThread.Post(() => { MainView.InvalidateVisual(); });
}, CancellationToken.None, TaskContinuationOptions.None, TaskScheduler.Default);

return Dispatcher.UIThread.InvokeAsync(() => { }).GetTask();
}

#pragma warning disable CA1822
// ReSharper disable once MemberCanBeMadeStatic.Global It can be static only because we rely on static locator currently. I bet it will change in the future
public bool IsRgbColorMode()
#pragma warning restore CA1822
{
IConsoleColorMode consoleColorMode = AvaloniaLocator.Current.GetService<IConsoleColorMode>()
?? throw new ConsoloniaException(
"Console color mode has not been initialized");

return consoleColorMode is RgbConsoleColorMode;
}

protected virtual void Dispose(bool disposing)
{
if (!_disposedValue)
{
if (disposing)
{
// TODO: dispose managed state (managed objects)
_cts?.Dispose();
_cts = null;
}

// TODO: free unmanaged resources (unmanaged objects) and override finalizer
// TODO: set large fields to null
_disposedValue = true;
}
}

// // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources
// ~ConsoloniaLifetime()
// {
// // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
// Dispose(disposing: false);
// }

public void Dispose()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
Dispose(true);
GC.SuppressFinalize(this);
}
}
}
Loading
Loading