Open Lakritzator opened 3 years ago
WinForms/WPF don't require to be initialized, they are available and ready to use even before generic host is configured.
Avalonia requires a specific initialization procedure and specific platform-dependent startup and lifetime management (i. e. we don't control application lifetime when running on iOS or Android).
You can investigate the possibilities of running on generic host, but that will require re-implementing AppBuilder on top of it.
I have a worker service (using the generic host) that I wanted to add a UI for. I looked for cross platform UI's and thought I'd give avalonia a try. My worker service runs as any of:
When bringing in Avalonia, I have ended up with code like the following in my main entry point. I have only tested this when running as a console app so far, so I am hoping it will work when running under systemd or windows service but that remains to be seen:
private static Task _backgroundHostTask;
private static Task<int> _uiTask;
public static async Task<int> Main(string[] args)
{
_backgroundHostTask = CreateHostBuilder(args).Build().RunAsync();
_uiTask = Task.Run(() =>
{
var app = BuildAvaloniaApp();
return app.StartWithClassicDesktopLifetime(args, ShutdownMode.OnExplicitShutdown);
});
await _backgroundHostTask;
var lifetime = Application.Current.ApplicationLifetime as IControlledApplicationLifetime;
lifetime?.Shutdown(0);
var result = await _uiTask;
return result;
}
I think there is an opportunity here for some better integration with the generic host, when you want to show a UI that is directly tied to the lifetime of the host.
The convention of the generic host is that:-
Typically a Startup
class is implemented in .net core projects to allow the configuration of 2 and 3.
I could envisage a step being added to allow the Startup class to build the avalonia app using AppBuilder:
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
// for DI etc.
}
public void Configure(IAppBuilder app)
{
// for configuring middleware if hosting web services etc.
}
public void ConfigureUI(AppBuilder avaloniaAppBuilder)
{
// avalonia stuff here?
}
}
I think something like this would feel more natural to existing .net core projects wishing to add a UI e.g:
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.UseStartupWithUi<MyStartup>() // just an example showing one possible approach.
.UseAvaloniaApp(); // causes the avalonia app to terminate with the host.
Something kind of extension methods like the ones demonstrated above would enable:
Forgive this pretty high level speculation around what the api's might look like, I don't have any concrete ideas but feel there is room for some possible enhancements in this area.
@dazinator in https://github.com/dapplo/Dapplo.Microsoft.Extensions.Hosting I already made such UI workloads for WPF and Windows Forms possible on the generic host.
I'm not so well acquainted with Avalonia yet, and have a big backlog for releasing the next Greenshot. I would love to have a sample project with Avalonia, maybe you like to fit it in with a PR?
Have a look at https://github.com/dapplo/Dapplo.Microsoft.Extensions.Hosting/tree/master/src/Dapplo.Microsoft.Extensions.Hosting.Wpf how I made it possible for Wpf, the sample using this is here: https://github.com/dapplo/Dapplo.Microsoft.Extensions.Hosting/blob/master/samples/Dapplo.Hosting.Sample.WpfDemo/Program.cs#L60
Hi there,
sorry for getting back to after such a long time.
I actually stumbled across this issue when I tried to combine an GRPC Server with Avalonia.
My solution consists of some simple extension methods:
[SupportedOSPlatform("windows")]
[SupportedOSPlatform("linux")]
[SupportedOSPlatform("macos")]
public static IHostBuilder ConfigureAvaloniaAppBuilder<TApplication>(this IHostBuilder hostBuilder, Func<AppBuilder> appBuilderResolver, Action<AppBuilder> configureAppBuilder, IHostedLifetime? lifetime = null) where TApplication: Avalonia.Application
{
ArgumentNullException.ThrowIfNull(configureAppBuilder);
hostBuilder.ConfigureServices((ctx, s) => {
AppBuilder appBuilder = appBuilderResolver();
configureAppBuilder(appBuilder);
s.AddSingleton(appBuilder);
if (appBuilder.Instance is null)
{
appBuilder.SetupWithoutStarting();
}
s.AddSingleton<Avalonia.Application>((_) => appBuilder.Instance!);
s.AddSingleton<TApplication>((_) => (TApplication)appBuilder.Instance!);
s.AddSingleton<IHostedLifetime>(p => lifetime ?? HostedLifetime.Select(p.GetRequiredService<ILoggerFactory>(), p.GetRequiredService<Avalonia.Application>().ApplicationLifetime));
});
return hostBuilder;
}
and
[SupportedOSPlatform("windows")]
[SupportedOSPlatform("linux")]
[SupportedOSPlatform("macos")]
public async static Task<int> RunAvaloniaAppAsync(this IHost host, CancellationToken token = default)
{
IHostedLifetime lifetime = host.Services.GetRequiredService<IHostedLifetime>();
Avalonia.Application application = host.Services.GetRequiredService<Application>();
await host.StartAsync(token);
int result = await lifetime.StartAsync(application, token);
await host.StopAsync(token);
await host.WaitForShutdownAsync(token);
return result;
}
whereas the IHostedLifetime
interface is a wrapper around the Avalonia IApplicationLifetime
.
Hence, my Program.Main.cs of the CommandSample now looks like this:
[STAThread]
public static async Task<int> Main(string[] args)
{
if (!OperatingSystem.IsWindows() && !OperatingSystem.IsLinux())
{
return -1;
}
return await Host.CreateDefaultBuilder(args).ConfigureAvalonia<App>(
a =>
{
a.UsePlatformDetect().LogToTrace().UseReactiveUI();
a.SetupWithLifetime(
new ClassicDesktopStyleApplicationLifetime()
{
Args = args,
ShutdownMode = Avalonia.Controls.ShutdownMode.OnMainWindowClose,
}
);
}
)
.ConfigureServices(s => s.AttachLoggerToAvaloniaLogger())
.Build().RunAvaloniaAppAsync();
}
I think this is a good starting point as it actually wraps Avalonia around the generic host builder.
@kekekeks @MikeCodesDotNET
What do you think about this solution? Would you like to know more? Is this an acceptable way of running Avalonia with the generic host?
Regards Carsten
@carstencodes could you please also show the implementation of HostedLifetime
?
@ArcadeArchie Hi, Let me get back to you. I have talked to my manager about some points. We're currently in discussion.
@carstencodes @ArcadeArchie for the most part you can integrate MsHosting in a way easier way: https://github.com/AvaloniaUI/Avalonia.Samples/pull/64/files#diff-bb94de43b9844a0fc00604842958cf32199ebfa63e6ad7b36db7f4b1f03a3767R24-R62
A couple of problems with other solutions:
Having these problems in mind, it's way easier to initialize Avalonia first, and then run Host Builder from it. Typically from OnFrameworkInitializationCompleted.
I will try to add some sample description to make it a proper Avalonia.Samples project. But code itself is ready and can be used.
- Main thread should still be the first thread to support macOS. Async main breaks it. Ok, noticed. (I'm not paying 1000€ just for testing purposes.), so see comment below:
- It also should work with previewer. Note, previewer doesn't run Main method, but it runs the same Application. Ok, this can be fixed quite easily ...
// Initialization code. Don't use any Avalonia, third-party APIs or any
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized
// yet and stuff might break.
[STAThread]
public static void Main(string[] args) => HostBuilder.CreateDefaultBuilder(args).SetupAvaloniaApp<App>(BuildAvaloniaApp, a => a.UseClassicDesktopRuntime(args, Avalonia.Controls.ShutdownMode.OnMainWindowClose)).ConfigureServices(s => s.AttachLoggerToAvaloniaLogger())
.Build().RunAvaloniaApp();
// Avalonia configuration, don't remove; also used by visual designer.
public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>()
.UsePlatformDetect()
.LogToTrace()
.UseReactiveUI();
At that point, there should not be a change to the behavior.
- Host.StartAsync/StopAsync are needed only if you want to have background services. It's not necessary for logging, DI, httpclient factory...
Yes, but if there are background services, it will be.
- In a cross-platform environment, you won't always have a "lifetime" object that will tell when application is closing. Mobile apps don't tell that. Browser doesn't as well. But on desktop you can use Avalonia IControlledApplicationLifetime.Exit event. Alalternatively, you can use try/finally block in Main method to handle app closure.
That's why I restricted the methods to desktop operating systems (see SupportedOSPlatformAttribute
)
Having these problems in mind, it's way easier to initialize Avalonia first, and then run Host Builder from it. Typically from OnFrameworkInitializationCompleted.
I've experienced issues with this approach, but at that point I was not that experienced with Avalonia. But migrating this approach to Avalonia caused me much headaches.
If the pitfalls are that high, how about adding a custom library helping developers to bypass these issues? Like Avalonia.Extensions.Hosting
or so?
Yep, there is nothing wrong with your approach since you know what actual target devices you need to run your app on. With little changes, it can work on macOS too, which would cover all desktop platforms.
The problem is that it won't be really easily possible to have universal Avalonia.Hosting library that covers all supported platforms. Different lifetimes support, different Main method specifics (sync only on macOS, async only on Browser...). As for now, I am planning to complete that samples page which can be used by developers as a reference, especially since it's simple and is enough for most apps.
But if the community implements Avalonia.Hosting deeper integration, for desktop only or more, would be also useful. It can be also mentioned as an integrated alternative approach in Avalonia.Samples.
@ArcadeArchie Sorry for coming back to you so late with this. I didn't forget about you (or this topic), but due to health conditions and many work, this moved a little to the bottom of the priority list.
I've created a gist containing my implementation hoping you find it interesting and helpful.
As @maxkatz6 stated, MacOS should use the sync revision, but I haven't tested it.
@maxkatz6 Glad to hear, that there is nothing wrong with it.
I am actually willing to do my part. So, if this is fine, maybe a first implementation only for desktop platforms can be set-up more easily from my implementation.
I am actually struggling with the idea of starting the host within the XPF Application, as - to me - it doesn't really fit in there. The application should from my side be target and not source for DI, but that is a question of personal taste and opinions - as it is usually done in Software Development.
An implementation of generic host for avaloniaui desktop app https://github.com/NeverMorewd/Hosting.Avaloniaui
Is your feature request related to a problem? Please describe. No, there is no problem.
Describe the solution you'd like Make it possible to run Avalonia on the generic host
Describe alternatives you've considered Not using the generic host.
Additional context I've made a project hosting Windows Forms and WPF in the generic host, which you can find here: https://github.com/dapplo/Dapplo.Microsoft.Extensions.Hosting I've added samples for CaliburnMicro and ReactiveUI, and wanted to add a sample for Avalonia to it, when I stumbled on https://github.com/AvaloniaUI/Avalonia/issues/3538
@kekekeks can I find more details on why the generic host, Microsoft.Extensions, isn't suitable for Avalonia? Is this an issue with the used IoC container, or the threading model, or what?
I'm not saying it's not possible, I just wouldn't like it if I ended up wrapping one "host" into another.