AvaloniaUI / Avalonia

Develop Desktop, Embedded, Mobile and WebAssembly apps with C# and XAML. The most popular .NET UI client technology
https://avaloniaui.net
MIT License
26.09k stars 2.26k forks source link

`StartWith[...]Lifetime` should dispose the `Application` instance #15142

Open Tyrrrz opened 8 months ago

Tyrrrz commented 8 months ago

Is your feature request related to a problem? Please describe.

My implementation of Application has a Dispose method that disposes the inner ServiceProvider that contains various domain-specific services and view models.

Application is not disposed automatically by Avalonia. So I have to run the app like this:

// Program.cs

public static AppBuilder BuildAvaloniaApp() =>
    AppBuilder.Configure<App>().UsePlatformDetect().LogToTrace();

[STAThread]
public static int Main(string[] args)
{
    var builder = BuildAvaloniaApp();

    try
    {
        return builder.StartWithClassicDesktopLifetime(args);
    }
    finally
    {
        // Clean up after application shutdown
        if (builder.Instance is IDisposable disposableApp)
            disposableApp.Dispose();
    }
}

This is a bit clunky. It would be much nicer if the underlying plumbing code automatically checked if the produced Application instance implements IDisposable and executed it if so. The AppBuilder is responsible for instantiating the object, so it makes sense it would also be responsible for its cleanup.

Describe the solution you'd like

I want the following code to execute Dispose() on my application automatically:

// Program.cs

public static AppBuilder BuildAvaloniaApp() =>
    AppBuilder.Configure<App>().UsePlatformDetect().LogToTrace();

[STAThread]
public static int Main(string[] args) =>
    BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);

Describe alternatives you've considered

The current alternative I use is described above. I also looked into implementing a custom Lifetime, but it looks like it's not very easy.

Additional context

Applications may need a dispose methods to release native resources that are not tied to a specific process (and as such won't be released automatically when the process exits), or when the application needs to reset some global state to its default or previous value. In my case, I need to reset monitor gamma to its initial state when the application exits.

maxkatz6 commented 8 months ago

I don't think we can reliably have that implemented. Sure, for ClassicDesktopLifetime it would be straightforward. But not all lifeimes have even a concept of being closed. And embedding scenarios (i.e. - without lifetime) won't have that either.

Promising that Application is going to be disposed on some platforms, will create similar expectations about other platforms.

Tyrrrz commented 8 months ago

Could that be an option that is specifically configured for classic desktop lifetime then? Something like:

[STAThread]
public static int Main(string[] args) =>
    BuildAvaloniaApp().StartWithClassicDesktopLifetime(args, disposeApplication: true);
wsficke commented 8 months ago

@Tyrrrz here's the recipe. You should use a custom app lifetime to hook your DI container. It's easy.

// Program.cs Main

        BuildAvaloniaApp().Start(App.AppMain, args);

// App.axaml.cs

        public static void AppMain(Application app, string[] commandLineArguments)
        {
            // build your DI container
            [proprietary code omitted]

            app.Run(ShutdownRequestCts.Token); // Run the UI event loop, observing a CancellationTokenSource that breaks the event loop

            // dispose your DI container
            [proprietary code omitted]
        }

Based on my research, Avalonia does not have an opinionated strategy for DI. The previewer depends on having a 0-parameter constructor on all your ViewModels. So if you want to use the previewer with real data, constructor injection is out.

I use explicit injection on most of my view models, having set up the DI container as a static property on App. Then, in the ViewModels, I set up field initializers that access App. [DI container property] .Resolve(...)

jp2masa commented 8 months ago

The previewer depends on having a 0-parameter constructor on all your ViewModels. So if you want to use the previewer with real data, constructor injection is out.

You can use constructor injection if you implement an approach as the one described in https://github.com/AvaloniaUI/Avalonia/discussions/13743#discussioncomment-7686499, for example.