dotnet / winforms

Windows Forms is a .NET UI framework for building Windows desktop applications.
MIT License
4.28k stars 955 forks source link

Add WindowsFormsLifetime extensions to the .NET Generic Host #11415

Open alex-oswald opened 1 month ago

alex-oswald commented 1 month ago

Background and motivation

The .NET Generic Host is the standard for developing .NET applications that leverage common .NET libraries such as Dependency Injection, Configuration and Logging. Currently, .NET does not provide a built in way for Windows Forms developers to use the Generic Host. I believe most Windows Forms developers use the standard template that comes with Visual Studio when creating Windows Forms applications. This template uses old .NET patterns and isn't up to date with the new Minimal API pattern added to .NET.

While working in the Enterprise world, I worked on a lot of internal Windows Forms applications that we were modernizing and upgrading to .NET Core at the time. There was no easy way to use dependency injection, configuration, logging, and background task support. This need drove me to write the WindowsFormsLifetime library that provides extensions for the Generic Host to support Windows Forms and its lifetime. I believe there is a want and need for this functionality world wide, and building it into .NET would allow Windows Forms developers everywhere the chance to use modern patterns and libraries they use in other places.

High Level

Windows Forms Lifetime works by registering a custom implementation of IHostLifetime for the generic host. It also registers an IHostedService that creates and manages the GUI thread that Windows Forms works on top of, and hooks into Windows Forms Application context to shut down the host when the application exits.

Going Deep

A high level explanation of how Windows Forms Lifetime was described above. This section will go into detail of the inner workings. Lets hope I understand this the same as when I wrote the library since it's been 4 years since my initial proof of concept.

To understand how this works, you need to have a beginners understanding of how Windows Forms works internally. WinForms uses an Application class with a bunch of static methods that manages the application and the UI thread. The Application class helps manage an ApplicationContext. This context controls the application by listening to the main forms close event and then exits the GUI threads message loop that is reference by the ThreadContext. When you write a Windows Forms application, you invoke Application.Run() to start the GUI thread and show the main form. When you exit the main form, the ApplicationContext is notified and exits the GUI thread. Using this information, we can modify how Application works and use our own GUI thread. So lets get into it.

The Generic Host implements a singleton of IHostLifetime. In the standard Generic Host implementation, IHostLifetime is never messed with, but since we want to control the lifetime of the application, we first create a custom implementation of the interface called WindowsFormsLifetime. Injecting our own implementation of IHostLifetime overrides the default implementation and lets us control when the Generic Host shuts down.

We also add the WindowsFormsHostedService IHostedService implementation. This is really the core of the library. The Generic Host starts up every IHostedService registered and they can effectively run for the entirety of the application. When WindowsFormsHostedService is started, it creates a thread for the GUI and passes in the StartUiThread method. The StartUiThread method also registers a callback with Application.ApplicationExit that signals the Generic Host to shutdown when the WinForms application exits, i.e. the main form is closed. The StartUiThread also captures the WindowsFormsSynchronizationContext for the GUI thread and saves it to the singleton WindowsFormsSynchronizationContextProvider for easy retrieval later using dependency injection. Lastly, the StartUiThread method gets the ApplicationContext registered with the service provider and passes it to the Application.Run() invocation.

As mentioned above, an instance of WindowsFormsSynchronizationContextProvider is registered with the service provider for easy retrieval in various parts of the application. This class also inherits the IGuiContext interface that exposes methods to invoke Action's and Func's with the WindowsFormsSynchronizationContext to ensure they are invoked on the GUI thread.

An implementation of IFormProvider is also registered with the service provider. This implementation provides an easy way to fetch a new instance of a form from the service provider and ensures it is created on the GUI thread. It exposes a few methods such as, GetFormAsync<T>() and GetFormAsync<T>(IServiceScope scope) allowing the creation of scoped forms. The advantage here is that you can create a form with the same scope as a DbContext instance. This allows the DbContext instance to be disposed of when the form is closed.

API Proposal

The main API proposal adds extension methods to the Generic Host. A few IHostApplicationBuilder extensions would be created in the Microsoft.Extensions.Hosting namespace.

namespace Microsoft.Extensions.Hosting;

public static class WindowsFormsLifetimeHostApplicationBuilderExtensionsa
{
    public static IHostApplicationBuilder UseWindowsFormsLifetime<TStartForm>(
        this IHostApplicationBuilder hostAppBuilder,
        Action<WindowsFormsLifetimeOptions> configure = null)
        where TStartForm : Form

    public static IHostApplicationBuilder UseWindowsFormsLifetime<TAppContext>(
        this IHostApplicationBuilder hostAppBuilder,
        Func<TAppContext> applicationContextFactory = null,
        Action<WindowsFormsLifetimeOptions> configure = null)
        where TAppContext : ApplicationContext

    public static IHostApplicationBuilder UseWindowsFormsLifetime<TAppContext, TStartForm>(
        this IHostApplicationBuilder hostAppBuilder,
        Func<TStartForm, TAppContext> applicationContextFactory,
        Action<WindowsFormsLifetimeOptions> configure = null)
        where TAppContext : ApplicationContext
        where TStartForm : Form
}

A few IServiceCollection extensions would be created in the Microsoft.Extensions.DependencyInjection namespace.

namespace Microsoft.Extensions.DependencyInjection;

public static class WindowsFormsLifetimeServiceCollectionExtensions
{
    public static IServiceCollection AddWindowsFormsLifetime(
        this IServiceCollection services,
        Action<WindowsFormsLifetimeOptions> configure,
        Action<IServiceProvider> preApplicationRunAction = null)

    public static IServiceCollection AddWindowsFormsLifetime<TStartForm>(
        this IServiceCollection services,
        Action<WindowsFormsLifetimeOptions> configure = null,
        Action<IServiceProvider> preApplicationRunAction = null)
        where TStartForm : Form

    public static IServiceCollection AddWindowsFormsLifetime<TAppContext>(
        this IServiceCollection services,
        Func<TAppContext> applicationContextFactory = null,
        Action<WindowsFormsLifetimeOptions> configure = null,
        Action<IServiceProvider> preApplicationRunAction = null)
        where TAppContext : ApplicationContext

    public static IServiceCollection AddWindowsFormsLifetime<TAppContext, TStartForm>(
        this IServiceCollection services,
        Func<TStartForm, TAppContext> applicationContextFactory,
        Action<WindowsFormsLifetimeOptions> configure = null,
        Action<IServiceProvider> preApplicationRunAction = null)
        where TAppContext : ApplicationContext
        where TStartForm : Form
}

Registered Services

IOptions<WindowsFormsLifetimeOptions>

public HighDpiMode HighDpiMode { get; set; } = HighDpiMode.SystemAware;
public bool EnableVisualStyles { get; set; } = true;
public bool CompatibleTextRenderingDefault { get; set; }
public bool SuppressStatusMessages { get; set; }
public bool EnableConsoleShutdown { get; set; }

IHostApplicationLifetime via WindowsFormsLifetime

Replaces the hosts singleton lifetime implementation.

IWindowsFormsSynchronizationContextProvider via WindowsFormsSynchronizationContextProvider

Holds a reference to the WindowsFormsSynchronizationContext for the Windows Forms thread.

WindowsFormsSynchronizationContext SynchronizationContext { get; }

IWindowsFormsThreadContext via WindowsFormsSynchronizationContextProvider

Provides methods to marshal calls to the WindowsFormsSynchronizationContext, i.e. the Windows Forms thread.

void Invoke(Action action);
TResult Invoke<TResult>(Func<TResult> func);
Task<TResult> InvokeAsync<TResult>(Func<TResult> func);
Task<TResult> InvokeAsync<TResult, TInput>(Func<TInput, TResult> func, TInput input);

IFormProvider via FormProvider

Task<T> GetFormAsync<T>() where T : Form;
Task<T> GetFormAsync<T>(IServiceScope scope) where T : Form;
Task<Form> GetMainFormAsync();
T GetForm<T>() where T : Form;
T GetForm<T>(IServiceScope scope) where T : Form;

API Usage

Currently, when you create a Windows Forms project in C# using the template, you get the following Program.cs file:

namespace WinFormsApp1
{
    internal static class Program
    {
        /// <summary>
        ///  The main entry point for the application.
        /// </summary>
        [STAThread]
        static void Main()
        {
            // To customize application configuration such as set high DPI settings or default font,
            // see https://aka.ms/applicationconfiguration.
            ApplicationConfiguration.Initialize();
            Application.Run(new Form1());
        }
    }
}

Using the new API, Program.cs would look like the following:

using Microsoft.Extensions.Hosting;
using WinFormsApp1;

var builder = Host.CreateApplicationBuilder(args);
builder.UseWindowsFormsLifetime<Form1>();
var app = builder.Build();
app.Run();

This allows the use of dependency injection. Here is an example of a forms code.

https://github.com/alex-oswald/WindowsFormsLifetime/blob/main/samples/SampleApp/Form1.cs

using Microsoft.Extensions.Logging;
using WindowsFormsLifetime;

namespace SampleApp;

public partial class Form1 : Form
{
    private readonly ILogger<Form1> _logger;
    private readonly IFormProvider _formProvider;

    public Form1(ILogger<Form1> logger, IFormProvider formProvider)
    {
        InitializeComponent();
        _logger = logger;
        _formProvider = formProvider;

        ThreadLabel.Text = $"{Thread.CurrentThread.ManagedThreadId} {Thread.CurrentThread.Name}";
    }

    private async void button1_Click(object sender, EventArgs e)
    {
        _logger.LogInformation("Show");
        var form = await _formProvider.GetFormAsync<Form2>();
        form.Show();
    }

    private void button2_Click(object sender, EventArgs e)
    {
        _logger.LogInformation("Close");
        this.Close();
    }
}

Use Cases

Windows Forms Blazor Hybrid

var builder = WebApplication.CreateBuilder(args);
builder.Host.UseWindowsFormsLifetime<Form1>();
builder.Services.AddWindowsFormsBlazorWebView();

var app = builder.Build();
app.Run();

An app with hosted services that run background tasks.

var builder = Host.CreateApplicationBuilder(args);
builder.UseWindowsFormsLifetime<Form1>();
builder.Services.AddHostedService<FormSpawnHostedService>();
builder.Services.AddHostedService<TickingHostedService>();
builder.Services.AddTransient<Form2>();

var app = builder.Build();
app.Run();

Using Entity Framework with a Sqlite database.

var builder = Host.CreateApplicationBuilder(args);
builder.Host.UseWindowsFormsLifetime<MainForm>();

builder.Services.AddScoped<IRepository<Note>, EntityFrameworkRepository<Note, SqliteDbContext>>();
builder.Services.AddDbContext<SqliteDbContext>(options =>
    options.UseSqlite(builder.Configuration.GetConnectionString("Default")));

var app = builder.Build();

// Create the database
var db = app.Services.GetService<SqliteDbContext>();
db?.Database.EnsureCreated();

app.Run();

Alternative Designs

Risks

Low, this should just enable easier modern development. Possible Windows Forms thread issues, though enough testing will mitigate this.

Will this feature affect UI controls?

No, this will not affect UI controls.

RussKie commented 1 month ago

Nice!

Few questions:

elachlan commented 1 month ago

I am excited for this :)

Are we able to pass additional parameters into the constructor? eg.

private async void button1_Click(object sender, EventArgs e)
{
    _logger.LogInformation("Show Form2");
    var entityId = GetEntityId(); // Replace with your logic to get the ID
    var form = await _formProvider.GetFormAsync<Form2>(entityId);
    form.Show();
}

or do we change to our own implementation using method injection?

private async void button1_Click(object sender, EventArgs e)
{
    _logger.LogInformation("Show Form2");
    var entityId = GetEntityId(); // Replace with your logic to get the ID
    var form = await _formProvider.GetFormAsync<Form2>();
    form.Initialize(entityId);
    form.Show();
}
alex-oswald commented 1 month ago
  • Where/when would ApplicationConfiguration.Initialize(); be invoked?

This is just a helpful method to consolidate what was more verbose. In my current library they are included as an option.

builder.UseWindowsFormsLifetime<Form1>(options =>
{
    options.HighDpiMode = HighDpiMode.SystemAware;
    options.EnableVisualStyles = true;
    options.CompatibleTextRenderingDefault = false;
    options.SuppressStatusMessages = false;
    options.EnableConsoleShutdown = true;
});
  • IIUIC, this design allowes forms and user controls to participate in DI, i.e., this becomes a possibility, right?
    public partial class MyForm : Form
    {
      public MyForm(IServiceProvider serviceProvider)
      {
          InitializeComponent();
      }
    }

    If my understanding is correct, then this would be good to call out explicitly and provide exmples.

Yes of course! I updated the proposal to make that more obvious and included an example.

  • The above would also affects the VS designer as the designer doesn't understand non-default .ctors, and either the designer has to change (which is unlikely) or we'll need to provide an alternative solution (e.g., this is how I solved it in Git Extensions).

I haven't had any issues with the designer.

alex-oswald commented 1 month ago

I am excited for this :)

Are we able to pass additional parameters into the constructor? eg.

private async void button1_Click(object sender, EventArgs e)
{
    _logger.LogInformation("Show Form2");
    var entityId = GetEntityId(); // Replace with your logic to get the ID
    var form = await _formProvider.GetFormAsync<Form2>(entityId);
    form.Show();
}

or do we change to our own implementation using method injection?

private async void button1_Click(object sender, EventArgs e)
{
    _logger.LogInformation("Show Form2");
    var entityId = GetEntityId(); // Replace with your logic to get the ID
    var form = await _formProvider.GetFormAsync<Form2>();
    form.Initialize(entityId);
    form.Show();
}

You can't pass parameters in like that. But you can inject services into a forms constructor. You could create some data bag that is registered with the service provider, inject it into one form and update data, then inject it in another form and register some event for when the data changes.

alex-oswald commented 1 month ago

Let me work on adding a section to the proposal that includes a list of services that get registered with the service provider and are available for use.

RussKie commented 1 month ago

I haven't had any issues with the designer.

The designer won't be able to render this form on the design surface unless there's a default .ctor:

public partial class MyForm : Form
{
    public MyForm(IServiceProvider serviceProvider)
    {
        InitializeComponent();
    }
}

This means that develoeprs will have to create default .ctors by hand (i.e., boilerplate).

davidfowl commented 1 month ago

This should be in another assembly, not the core hosting assembly. Microsoft.Extensions.Hosting.Winforms.

alex-oswald commented 1 month ago

This should be in another assembly, noy the core hosting assembly. Microsoft.Extensions.Hosting.Winforms.

Agreed. This depends on the windows target framework so it shouldn't be included in https://www.nuget.org/packages/Microsoft.Extensions.Hosting.

alex-oswald commented 1 month ago

This should be in another assembly, noy the core hosting assembly. Microsoft.Extensions.Hosting.Winforms.

Agreed. This depends on the windows target framework so it shouldn't be included in https://www.nuget.org/packages/Microsoft.Extensions.Hosting.

I haven't had any issues with the designer.

The designer won't be able to render this form on the design surface unless there's a default .ctor:

public partial class MyForm : Form
{
    public MyForm(IServiceProvider serviceProvider)
    {
        InitializeComponent();
    }
}

This means that develoeprs will have to create default .ctors by hand (i.e., boilerplate).

Hmm. I don't have default constructors in my sample apps and the designer works just fine with those forms. What exactly isn't supposed to work?

image
weltkante commented 1 month ago

An implementation side note:

You may be able to sideload these into UseWindowsFormsLifetime but maybe it should be more explicit that setting process-wide defaults are done as a side effect? I'd suggest allowing the user to call initialization separately and (maybe UseWindowsFormsLifetime does it implicitly if the user doesn't do it manually, instead of throwing, or something like this).

elachlan commented 1 month ago

Would there be impacts on our AOT work?

alex-oswald commented 1 month ago

An implementation side note:

  • The thread needs to be turned into STA really early since you no longer have the [STAThread] attribute on it. This is required so any COM objects created during initilization get initialized in the correct apartment for how they are going to be used later.
  • Same goes for ApplicationConfiguration.Initialize - it needs to do some important early initialization that other initialization code may require to be set up before the host starts running.

You may be able to sideload these into UseWindowsFormsLifetime but maybe it should be more explicit that setting process-wide defaults are done as a side effect? I'd suggest allowing the user to call initialization separately and (maybe UseWindowsFormsLifetime does it implicitly if the user doesn't do it manually, instead of throwing, or something like this).

This should all be taken care of early on. See the below snippet of my current implementation. WinForms hasn't been my main focus for a while though so if I'm missing something let me know.

    public Task StartAsync(CancellationToken cancellationToken)
    {
        _applicationStoppingRegistration = _hostApplicationLifetime.ApplicationStopping.Register(state =>
        {
            ((WindowsFormsHostedService)state).OnApplicationStopping();
        },
        this);

        Thread thread = new(StartUiThread)
        {
            Name = "WindowsFormsLifetime UI Thread"
        };
        thread.SetApartmentState(ApartmentState.STA);
        thread.Start();

        return Task.CompletedTask;
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        return Task.CompletedTask;
    }

    private void StartUiThread()
    {
        Application.SetHighDpiMode(_options.HighDpiMode);
        if (_options.EnableVisualStyles)
        {
            Application.EnableVisualStyles();
        }
        Application.SetCompatibleTextRenderingDefault(_options.CompatibleTextRenderingDefault);
        Application.ApplicationExit += OnApplicationExit;

        // Don't autoinstall since we are creating our own
        WindowsFormsSynchronizationContext.AutoInstall = false;

        // Create the sync context on our UI thread
        _syncContextManager.SynchronizationContext = new WindowsFormsSynchronizationContext();
        SynchronizationContext.SetSynchronizationContext(_syncContextManager.SynchronizationContext);

        var applicationContext = _serviceProvider.GetService<ApplicationContext>();
        PreApplicationRunAction?.Invoke(_serviceProvider);
        Application.Run(applicationContext);
    }

https://github.com/alex-oswald/WindowsFormsLifetime/blob/main/src/WindowsFormsLifetime/WindowsFormsHostedService.cs

alex-oswald commented 1 month ago

Would there be impacts on our AOT work?

I'm not sure. The implementation only relies on the windows target framework and the Microsoft.Extensions.Hosting nuget package.

weltkante commented 1 month ago

This should all be taken care of early on. See the below snippet of my current implementation.

The main problem of starting a separate thread is that developers may assume the main thread (on which the application is started and configured) will be used as the UI thread and they may initialize things on the main thread during container initialization. Besides having native objects attached to the wrong thread this can be fatal for other reasons as well - we once have had this cause the finalizer thread to deadlock because WinForms was called during initialization but this initializing thread wasn't afterwards becoming the UI thread and the internal COM objects WinForms accessed could not return to the startup thread for finalization, effectively hanging the finalizer forever and causing every finalizable object to leak.

Another issue is that some "application startup" calls WinForms needs to do are process-wide and not thread-wide, so doing them when starting up your thread may be too late, and any code needing to interact with WinForms before that point may see wrong defaults (e.g. Font measurements), or worse, permanently lock-in the wrong defaults (some defaults can't be changed once certain WinForms calls have been made).

Currently WinForms doesn't have a good way to detect or prevent calls that happen "too early", i.e. before the defaults have been configured or the UI thread has been selected. Ideally WinForms would keep track of initialization and throw exceptions if WinForms APIs are used before the basic configuration happened, but I doubt that's an easy change to make now.

alex-oswald commented 1 month ago

This should all be taken care of early on. See the below snippet of my current implementation.

The main problem of starting a separate thread is that developers may assume the main thread (on which the application is started and configured) will be used as the UI thread and they may initialize things on the main thread during container initialization. Besides having native objects attached to the wrong thread this can be fatal for other reasons as well - we once have had this cause the finalizer thread to deadlock because WinForms was called during initialization but this initializing thread wasn't afterwards becoming the UI thread and the internal COM objects WinForms accessed could not return to the startup thread for finalization, effectively hanging the finalizer forever and causing every finalizable object to leak.

Another issue is that some "application startup" calls WinForms needs to do are process-wide and not thread-wide, so doing them when starting up your thread may be too late, and any code needing to interact with WinForms before that point may see wrong defaults (e.g. Font measurements), or worse, permanently lock-in the wrong defaults (some defaults can't be changed once certain WinForms calls have been made).

Currently WinForms doesn't have a good way to detect or prevent calls that happen "too early", i.e. before the defaults have been configured or the UI thread has been selected. Ideally WinForms would keep track of initialization and throw exceptions if WinForms APIs are used before the basic configuration happened, but I doubt that's an easy change to make now.

All good points. I will add these to the Risks section. Perhaps documentation should explain this clearly. An analyzer could also be packaged that warns the user of doing anything related to winforms before the host starts up.

There is also the potential that some service tries to interact with the winforms thread before the hosted service starts up. Perhaps I should find a way to guarantee it starts before other hosted services.

To all these points though, developers would of course need to follow some guidance when using this feature for it to work properly, like anything else.

RussKie commented 1 month ago

Hmm. I don't have default constructors in my sample apps and the designer works just fine with those forms. What exactly isn't supposed to work?

I stand corrected. This must have changed because it used to fail. That's great!

weltkante commented 1 month ago

All good points. I will add these to the Risks section. Perhaps documentation should explain this clearly. An analyzer could also be packaged that warns the user of doing anything related to winforms before the host starts up.

I still think you underestimate the problem. Due to the builder pattern employed for setting up and configuring everything "before the host starts up" can involve a lot of user-defined delegates, much of which is executed during startup but on the main thread, and delegates are not something an analyzer can understand well. Also any constructor of objects registered in the service container is part of the startup; I don't think an analyzer can reasonably inspect the constructor of third party classes to determine whether its safe to register them.

Thinking about this, how will people actually write any DI service interacting with WinForms? The proposed API does not expose the UI thread and as mentioned above you have no control or indication if a service is constructed on the UI thread or on the main thread (for service constructors it'll be more or less random, depending on who resolves the service first).

I think this proposal needs to, at the very least, include a way for a service to run some initialization code on the UI thread. Maybe something in the direction of IHostedService for UI. If you have that you could say (and document) its the only supported way and starting to interact with WinForms any other way before the host finished startup is "unsupported". Still not great, since it provides no means to check if you comply to the rules, but I have no idea how to retrofit that and I understand people prefer an unsafe/brittle implementation over no implementation.

Personally I think turning the main thread into the UI thread is the better solution, even if its more work implementing it. (Though of course then you have the inverse problem, that all services will default to the WinForms SynchronizationContext, even if they are UI-agnostic - but on Desktop Framework this was always chosen as the default when designing new features, probably because its the more robust thing to do.)

IIUIC, this design allowes forms and user controls to participate in DI, i.e., this becomes a possibility, right?

Yes of course! I updated the proposal to make that more obvious and included an example.

This runs into the exact problem and is a great example of people not understanding the underlying issue.

alex-oswald commented 1 month ago

All good points. I will add these to the Risks section. Perhaps documentation should explain this clearly. An analyzer could also be packaged that warns the user of doing anything related to winforms before the host starts up.

I still think you underestimate the problem. Due to the builder pattern employed for setting up and configuring everything "before the host starts up" can involve a lot of user-defined delegates, much of which is executed during startup but on the main thread, and delegates are not something an analyzer can understand well. Also any constructor of objects registered in the service container is part of the startup; I don't think an analyzer can reasonably inspect the constructor of third party classes to determine whether its safe to register them.

I understand the analyzer issues. Thanks for pointing those out.

Thinking about this, how will people actually write any DI service interacting with WinForms? The proposed API does not expose the UI thread and as mentioned above you have no control or indication if a service is constructed on the UI thread or on the main thread (for service constructors it'll be more or less random, depending on who resolves the service first).

I updated the proposal to include the services that are registered with the service provider, including services that hold a reference to the WindowsFormsSynchronizationContext and another that provides the ability to invoke calls on the Windows Forms thread from anywhere.

I think this proposal needs to, at the very least, include a way for a service to run some initialization code on the UI thread. Maybe something in the direction of IHostedService for UI. If you have that you could say (and document) its the only supported way and starting to interact with WinForms any other way before the host finished startup is "unsupported". Still not great, since it provides no means to check if you comply to the rules, but I have no idea how to retrofit that and I understand people prefer an unsafe/brittle implementation over no implementation.

Good idea. I will work on that.

Personally I think turning the main thread into the UI thread is the better solution, even if its more work implementing it. (Though of course then you have the inverse problem, that all services will default to the WinForms SynchronizationContext, even if they are UI-agnostic - but on Desktop Framework this was always chosen as the default when designing new features, probably because its the more robust thing to do.)

I think this is a bad idea, for the reason you pointed out. I implemented this solution the way I did so the UI thread isn't easy to saturate. Consumers should follow best practices if they decide to use the library.

merriemcgaw commented 1 month ago

This is a great suggestion. We really want to take this (with some more conversation about design and implementation details) , but given current priorities we think this would fit much better in the .NET 10 timeframe. Please continue the conversation! We really like where this is going 😄

RussKie commented 1 month ago

@merriemcgaw this could probably be taken in .NET 9 under the ExperiementalAttribute, meaning that if it doesn't pan out - it can be completely reworked or nuked in future .NET versions. Assuming, of course, @alex-oswald is willing to do the heavy lifting.

merriemcgaw commented 3 weeks ago

I'll check with the team and circle back.

alex-oswald commented 3 weeks ago

@RussKie Yes, I am happy to do the heavy lifting. I will start working on the integration in a fork while keeping this proposal up to date. I'm going to have questions for sure. @merriemcgaw @KlausLoeffelmann should I ask detailed questions here for visibility, or would you rather me reach out via Teams?

JeremyKuhne commented 3 weeks ago

@alex-oswald What public API changes would this need in WinForms itself? Can we detail the proposed changes for that part of it so we can figure out the mechanics of possibly doing a preview for .NET 9?

alex-oswald commented 3 weeks ago

@alex-oswald What public API changes would this need in WinForms itself? Can we detail the proposed changes for that part of it so we can figure out the mechanics of possibly doing a preview for .NET 9?

There shouldn't need to be any changes to current APIs. There would only be additions.

JeremyKuhne commented 3 weeks ago

@alex-oswald What public API changes would this need in WinForms itself? Can we detail the proposed changes for that part of it so we can figure out the mechanics of possibly doing a preview for .NET 9?

There shouldn't need to be any changes to current APIs. There would only be additions.

@alex-oswald So no modifications of any existing types?

alex-oswald commented 2 weeks ago

@alex-oswald What public API changes would this need in WinForms itself? Can we detail the proposed changes for that part of it so we can figure out the mechanics of possibly doing a preview for .NET 9?

There shouldn't need to be any changes to current APIs. There would only be additions.

@alex-oswald So no modifications of any existing types?

Correct. The proposal is essentially to move the library I've created at https://github.com/alex-oswald/WindowsFormsLifetime into .NET officially, with some tweaks and name changes. Since the current library works standalone, nothing should need to be modified. Though I'm curious on what the process is to add support for its dependency. Since this proposal is the glue between WinForms and the .NET Generic Host, it has a dependency on Microsoft.Extensions.Hosting which is in the runtime repo.

RussKie commented 2 weeks ago

it has a dependency on Microsoft.Extensions.Hosting which is in the runtime repo.

This would be the most challenging part to work out: image

Windows Forms SDK is a part of the Windows Desktop SDK, which is installed alongside .NET SDK. However, M.E.H needs to be pulled in separately, which essentially creates an external dependency for Windows Forms...

This sounds like this feature will itself need to become an OOB offering, i.e., not shipped inbox but published to NuGet in the same fashion as M.E.H is (and other OOB packages).

alex-oswald commented 2 weeks ago

it has a dependency on Microsoft.Extensions.Hosting which is in the runtime repo.

This would be the most challenging part to work out: image

Windows Forms SDK is a part of the Windows Desktop SDK, which is installed alongside .NET SDK. However, M.E.H needs to be pulled in separately, which essentially creates an external dependency for Windows Forms...

This sounds like this feature will itself need to become an OOB offering, i.e., not shipped inbox but published to NuGet in the same fashion as M.E.H is (and other OOB packages).

Yes I agree. I think that is what @davidfowl was getting at when he said it should live in another assembly, such as Microsoft.Extensions.Hosting.WinForms. Now the question is, where should the code live? In the winforms or runtime repo?

RussKie commented 2 weeks ago

IMHO, dotnet/winforms is the place to be as the functionality is inherently tied to Windows Forms and any tests would be here too. My 2c, but the team may have different views on this.

elachlan commented 2 weeks ago

I'd like it to live in dotnet/winforms as it helps iterate on it faster.